├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── appveyor.yml ├── build.rs ├── desktop ├── dejavu_font │ ├── DejaVu Fonts License.txt │ ├── DejaVuSansMono-Bold.ttf │ ├── DejaVuSansMono-BoldOblique.ttf │ ├── DejaVuSansMono-Oblique.ttf │ └── DejaVuSansMono.ttf ├── org.daa.NeovimGtk-symbolic.svg ├── org.daa.NeovimGtk.desktop ├── org.daa.NeovimGtk.svg ├── org.daa.NeovimGtk_128.png └── org.daa.NeovimGtk_48.png ├── resources ├── neovim.ico └── side-panel.ui ├── runtime └── plugin │ └── nvim_gui_shim.vim ├── rustfmt.toml ├── screenshots └── neovimgtk-screen.png └── src ├── cmd_line.rs ├── color.rs ├── cursor.rs ├── dirs.rs ├── error.rs ├── file_browser.rs ├── grid.rs ├── highlight.rs ├── input.rs ├── main.rs ├── misc.rs ├── mode.rs ├── nvim ├── client.rs ├── ext.rs ├── handler.rs ├── mod.rs ├── redraw_handler.rs └── repaint_mode.rs ├── nvim_config.rs ├── plug_manager ├── manager.rs ├── mod.rs ├── plugin_settings_dlg.rs ├── store.rs ├── ui.rs ├── vim_plug.rs └── vimawesome.rs ├── popup_menu.rs ├── project.rs ├── render ├── context.rs ├── itemize.rs ├── mod.rs └── model_clip_iterator.rs ├── settings.rs ├── shell.rs ├── shell_dlg.rs ├── subscriptions.rs ├── sys ├── mod.rs ├── pango │ ├── attribute.rs │ └── mod.rs └── pangocairo │ └── mod.rs ├── tabline.rs ├── ui.rs ├── ui_model ├── cell.rs ├── item.rs ├── line.rs ├── mod.rs ├── model_layout.rs └── model_rect.rs └── value.rs /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | Description of what the bug is. 9 | 10 | **Technical information (please complete the following information):** 11 | - OS: [e.g. Windows, Linux Ubuntu] 12 | - Neovim version: [e.g. .0.0.3] 13 | - Neovim-Gtk build version: [e.g. flatpak, commit hash] 14 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Install dependencies 13 | run: sudo apt-get update && sudo apt-get install libgtk-3-dev 14 | - name: Build 15 | run: cargo build --verbose 16 | - name: Run tests 17 | run: cargo test --verbose 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.swp 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Unreleased 2 | 1. Migration to new ext_linegrid api [ui-linegrid](https://neovim.io/doc/user/ui.html#ui-linegrid) 3 | 2. New option --cterm-colors [#190](https://github.com/daa84/neovim-gtk/issues/190) 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nvim-gtk" 3 | version = "0.3.0" 4 | authors = ["daa84 "] 5 | build = "build.rs" 6 | edition = "2018" 7 | 8 | [dependencies] 9 | clap = "2.33" 10 | cairo-rs = "0.7" 11 | pango-sys = "0.9" 12 | pangocairo = "0.8" 13 | pangocairo-sys = "0.10" 14 | glib = "0.8.0" 15 | glib-sys = "0.9" 16 | gdk = "0.11" 17 | gdk-sys = "0.9" 18 | gio = "0.7.0" 19 | gobject-sys = "0.9" 20 | #gdk = { git = 'https://github.com/gtk-rs/gdk' } 21 | #gdk-sys = { git = 'https://github.com/gtk-rs/sys' } 22 | #glib = { git = 'https://github.com/gtk-rs/glib' } 23 | #glib-sys = { git = 'https://github.com/gtk-rs/sys' } 24 | #cairo-rs = { git = 'https://github.com/gtk-rs/cairo' } 25 | #cairo-sys-rs = { git = 'https://github.com/gtk-rs/cairo' } 26 | #pango = { git = 'https://github.com/gtk-rs/pango' } 27 | #pango-sys = { git = 'https://github.com/gtk-rs/sys' } 28 | #gio = { git = 'https://github.com/gtk-rs/gio' } 29 | #pangocairo = { git = 'https://github.com/RazrFalcon/pangocairo-rs' } 30 | neovim-lib = "0.6" 31 | phf = "0.8" 32 | log = "0.4" 33 | env_logger = "0.7" 34 | htmlescape = "0.3" 35 | rmpv = "0.4" 36 | percent-encoding = "1.0" 37 | regex = "1.3" 38 | lazy_static = "1.4" 39 | unicode-width = "0.1" 40 | unicode-segmentation = "1.6" 41 | fnv = "1.0" 42 | 43 | serde = "1.0" 44 | serde_derive = "1.0" 45 | toml = "0.5" 46 | serde_json = "1.0" 47 | 48 | atty = "0.2" 49 | dirs = "2.0" 50 | 51 | whoami = "0.6.0" 52 | 53 | [target.'cfg(unix)'.dependencies] 54 | unix-daemonize = "0.1" 55 | 56 | [target.'cfg(windows)'.build-dependencies] 57 | winres = "0.1" 58 | 59 | [build-dependencies] 60 | phf_codegen = "0.8" 61 | build-version = "0.1.1" 62 | 63 | [dependencies.pango] 64 | features = ["v1_38"] 65 | version = "0.7" 66 | 67 | [dependencies.gtk] 68 | version = "0.7" 69 | features = ["v3_22"] 70 | #git = "https://github.com/gtk-rs/gtk" 71 | 72 | [dependencies.gtk-sys] 73 | version = "0.9" 74 | features = ["v3_22"] 75 | #git = 'https://github.com/gtk-rs/sys' 76 | 77 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX?=/usr/local 2 | 3 | test: 4 | RUST_BACKTRACE=1 cargo test 5 | 6 | run: 7 | RUST_LOG=warn RUST_BACKTRACE=1 cargo run -- --no-fork 8 | 9 | install: install-resources 10 | cargo install --path . --force --root $(DESTDIR)$(PREFIX) 11 | 12 | install-debug: install-resources 13 | cargo install --debug --path . --force --root $(DESTDIR)$(PREFIX) 14 | 15 | install-resources: 16 | mkdir -p $(DESTDIR)$(PREFIX)/share/nvim-gtk/ 17 | cp -r runtime $(DESTDIR)$(PREFIX)/share/nvim-gtk/ 18 | mkdir -p $(DESTDIR)$(PREFIX)/share/applications/ 19 | sed -e "s|Exec=nvim-gtk|Exec=$(PREFIX)/bin/nvim-gtk|" \ 20 | desktop/org.daa.NeovimGtk.desktop \ 21 | >$(DESTDIR)$(PREFIX)/share/applications/org.daa.NeovimGtk.desktop 22 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/128x128/apps/ 23 | cp desktop/org.daa.NeovimGtk_128.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/128x128/apps/org.daa.NeovimGtk.png 24 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/ 25 | cp desktop/org.daa.NeovimGtk_48.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/org.daa.NeovimGtk.png 26 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/ 27 | cp desktop/org.daa.NeovimGtk.svg $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/ 28 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/symbolic/apps/ 29 | cp desktop/org.daa.NeovimGtk-symbolic.svg $(DESTDIR)$(PREFIX)/share/icons/hicolor/symbolic/apps/ 30 | 31 | .PHONY: all clean test 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neovim-gtk [![Build status](https://ci.appveyor.com/api/projects/status/l58o28e13f829llx/branch/master?svg=true)](https://ci.appveyor.com/project/daa84/neovim-gtk/branch/master)[![Build status](https://github.com/daa84/neovim-gtk/workflows/Ubuntu/badge.svg)](https://github.com/daa84/neovim-gtk/actions) 2 | GTK ui for neovim written in rust using gtk-rs bindings. With [ligatures](https://github.com/daa84/neovim-gtk/wiki/Configuration#ligatures) support. 3 | 4 | # Screenshot 5 | ![Main Window](/screenshots/neovimgtk-screen.png?raw=true) 6 | 7 | For more screenshots and description of basic usage see [wiki](https://github.com/daa84/neovim-gtk/wiki/GUI) 8 | 9 | # Configuration 10 | To setup font add next line to `ginit.vim` 11 | ```vim 12 | call rpcnotify(1, 'Gui', 'Font', 'DejaVu Sans Mono 12') 13 | ``` 14 | for more details see [wiki](https://github.com/daa84/neovim-gtk/wiki/Configuration) 15 | 16 | # Install 17 | ## From sources 18 | First check [build prerequisites](#build-prerequisites) 19 | 20 | By default to `/usr/local`: 21 | ``` 22 | make install 23 | ``` 24 | Or to some custom path: 25 | ``` 26 | make PREFIX=/some/custom/path install 27 | ``` 28 | 29 | ## archlinux 30 | AUR package for neovim-gtk https://aur.archlinux.org/packages/neovim-gtk-git 31 | ```shell 32 | git clone https://aur.archlinux.org/neovim-gtk-git.git 33 | cd neovim-gtk-git 34 | makepkg -si 35 | ``` 36 | ## openSUSE 37 | https://build.opensuse.org/package/show/home:mcepl:neovim/neovim-gtk 38 | 39 | ## windows 40 | Windows binaries on appveyor 41 | [latest build](https://ci.appveyor.com/api/projects/daa84/neovim-gtk/artifacts/nvim-gtk-mingw64.7z?branch=master) 42 | 43 | # Build prerequisites 44 | ## Linux 45 | First install the GTK development packages. On Debian/Ubuntu derivatives 46 | this can be done as follows: 47 | ``` shell 48 | apt install libgtk-3-dev 49 | ``` 50 | 51 | On Fedora: 52 | ```bash 53 | dnf install atk-devel cairo-devel gdk-pixbuf2-devel glib2-devel gtk3-devel pango-devel 54 | ``` 55 | 56 | Then install the latest rust compiler, best with the 57 | [rustup tool](https://rustup.rs/). The build command: 58 | ``` 59 | cargo build --release 60 | ``` 61 | 62 | ## Windows 63 | Neovim-gtk can be compiled using MSYS2 GTK packages. In this case use 'windows-gnu' rust toolchain. 64 | ``` 65 | SET PKG_CONFIG_PATH=C:\msys64\mingw64\lib\pkgconfig 66 | cargo build --release 67 | ``` 68 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | global: 3 | PROJECT_NAME: neovim-gtk-win64 4 | matrix: 5 | - TARGET: x86_64-pc-windows-gnu 6 | RUST_VERSION: 1.38.0 7 | 8 | install: 9 | # - ps: Start-FileDownload "https://static.rust-lang.org/dist/channel-rust-stable" 10 | # - ps: $env:RUST_VERSION = Get-Content channel-rust-stable | select -first 1 | %{$_.split('-')[1]} 11 | - SET RUST_URL=https://static.rust-lang.org/dist/rust-%RUST_VERSION%-%TARGET%.exe 12 | - SET PATH=C:\Rust\bin;C:\msys64\mingw64\bin;%PATH%;C:\msys64\usr\bin 13 | - SET PKG_CONFIG_PATH=C:\msys64\mingw64\lib\pkgconfig 14 | - ps: Start-FileDownload $env:RUST_URL -FileName rust-dist.exe 15 | - rust-dist.exe /VERYSILENT /NORESTART /COMPONENTS="Rustc,Gcc,Cargo,Std" /DIR="C:\Rust" 16 | - rustc -V 17 | - cargo -V 18 | - pacman --noconfirm -S mingw-w64-x86_64-gtk3 19 | # for icon 20 | - pacman --noconfirm -S mingw-w64-x86_64-binutils 21 | - pacman --noconfirm -S mingw-w64-x86_64-gcc 22 | 23 | build_script: 24 | - cargo test 25 | - cargo build --release 26 | 27 | after_build: 28 | - cmd: >- 29 | mkdir dist 30 | 31 | cd dist 32 | 33 | mkdir bin share lib 34 | 35 | mkdir share\glib-2.0 share\icons lib\gdk-pixbuf-2.0 36 | 37 | xcopy .\..\target\release\nvim-gtk.exe .\bin 1> nul 38 | 39 | for %%a in (C:\msys64\mingw64\bin\libfribidi-0.dll,C:\msys64\mingw64\bin\libatk-1.0-0.dll,C:\msys64\mingw64\bin\libbz2-1.dll,C:\msys64\mingw64\bin\libcairo-2.dll,C:\msys64\mingw64\bin\libcairo-gobject-2.dll,C:\msys64\mingw64\bin\libepoxy-0.dll,C:\msys64\mingw64\bin\libexpat-1.dll,C:\msys64\mingw64\bin\libffi-6.dll,C:\msys64\mingw64\bin\libfontconfig-1.dll,C:\msys64\mingw64\bin\libfreetype-6.dll,C:\msys64\mingw64\bin\libgcc_s_seh-1.dll,C:\msys64\mingw64\bin\libgdk-3-0.dll,C:\msys64\mingw64\bin\libgdk_pixbuf-2.0-0.dll,C:\msys64\mingw64\bin\libgio-2.0-0.dll,C:\msys64\mingw64\bin\libglib-2.0-0.dll,C:\msys64\mingw64\bin\libgmodule-2.0-0.dll,C:\msys64\mingw64\bin\libgobject-2.0-0.dll,C:\msys64\mingw64\bin\libgraphite2.dll,C:\msys64\mingw64\bin\libgtk-3-0.dll,C:\msys64\mingw64\bin\libharfbuzz-0.dll,C:\msys64\mingw64\bin\libiconv-2.dll,C:\msys64\mingw64\bin\libintl-8.dll,C:\msys64\mingw64\bin\libpango-1.0-0.dll,C:\msys64\mingw64\bin\libpangocairo-1.0-0.dll,C:\msys64\mingw64\bin\libpangoft2-1.0-0.dll,C:\msys64\mingw64\bin\libpangowin32-1.0-0.dll,C:\msys64\mingw64\bin\libpcre-1.dll,C:\msys64\mingw64\bin\libpixman-1-0.dll,C:\msys64\mingw64\bin\libpng16-16.dll,C:\msys64\mingw64\bin\libstdc++-6.dll,C:\msys64\mingw64\bin\libwinpthread-1.dll,C:\msys64\mingw64\bin\zlib1.dll,C:\msys64\mingw64\bin\libthai-0.dll,C:\msys64\mingw64\bin\libdatrie-1.dll) do xcopy %%a .\bin 1> nul 40 | 41 | xcopy C:\msys64\mingw64\share\glib-2.0 .\share\glib-2.0 /E 1> nul 42 | 43 | xcopy C:\msys64\mingw64\share\icons .\share\icons /E 1> nul 44 | 45 | xcopy C:\msys64\mingw64\lib\gdk-pixbuf-2.0 .\lib\gdk-pixbuf-2.0 /E 1> nul 46 | 47 | cd .. 48 | 49 | 7z a nvim-gtk-mingw64.7z dist\* 50 | 51 | artifacts: 52 | - path: nvim-gtk-mingw64.7z 53 | name: mingw64-bin 54 | 55 | test: false 56 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | extern crate build_version; 2 | extern crate phf_codegen; 3 | 4 | #[cfg(windows)] 5 | extern crate winres; 6 | 7 | use std::env; 8 | use std::fs::File; 9 | use std::io::{BufWriter, Write}; 10 | use std::path::Path; 11 | 12 | fn main() { 13 | build_version::write_version_file().expect("Failed to write version.rs file"); 14 | 15 | if cfg!(target_os = "windows") { 16 | println!("cargo:rustc-link-search=native=C:\\msys64\\mingw64\\lib"); 17 | 18 | set_win_icon(); 19 | } 20 | 21 | let path = Path::new(&env::var("OUT_DIR").unwrap()).join("key_map_table.rs"); 22 | let mut file = BufWriter::new(File::create(&path).unwrap()); 23 | 24 | writeln!( 25 | &mut file, 26 | "static KEYVAL_MAP: phf::Map<&'static str, &'static str> = \n{};\n", 27 | phf_codegen::Map::new() 28 | .entry("F1", "\"F1\"") 29 | .entry("F2", "\"F2\"") 30 | .entry("F3", "\"F3\"") 31 | .entry("F4", "\"F4\"") 32 | .entry("F5", "\"F5\"") 33 | .entry("F6", "\"F6\"") 34 | .entry("F7", "\"F7\"") 35 | .entry("F8", "\"F8\"") 36 | .entry("F9", "\"F9\"") 37 | .entry("F10", "\"F10\"") 38 | .entry("F11", "\"F11\"") 39 | .entry("F12", "\"F12\"") 40 | .entry("Left", "\"Left\"") 41 | .entry("Right", "\"Right\"") 42 | .entry("Up", "\"Up\"") 43 | .entry("Down", "\"Down\"") 44 | .entry("Home", "\"Home\"") 45 | .entry("End", "\"End\"") 46 | .entry("BackSpace", "\"BS\"") 47 | .entry("Return", "\"CR\"") 48 | .entry("Escape", "\"Esc\"") 49 | .entry("Delete", "\"Del\"") 50 | .entry("Insert", "\"Insert\"") 51 | .entry("Page_Up", "\"PageUp\"") 52 | .entry("Page_Down", "\"PageDown\"") 53 | .entry("Enter", "\"CR\"") 54 | .entry("Tab", "\"Tab\"") 55 | .entry("ISO_Left_Tab", "\"Tab\"") 56 | .build() 57 | ) 58 | .unwrap(); 59 | } 60 | 61 | #[cfg(windows)] 62 | fn set_win_icon() { 63 | let mut res = winres::WindowsResource::new(); 64 | res.set_icon("resources/neovim.ico"); 65 | if let Err(err) = res.compile() { 66 | eprintln!("Error set icon: {}", err); 67 | } 68 | } 69 | 70 | #[cfg(unix)] 71 | fn set_win_icon() {} 72 | -------------------------------------------------------------------------------- /desktop/dejavu_font/DejaVu Fonts License.txt: -------------------------------------------------------------------------------- 1 | Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. 2 | Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) 3 | 4 | Bitstream Vera Fonts Copyright 5 | ------------------------------ 6 | 7 | Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is 8 | a trademark of Bitstream, Inc. 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of the fonts accompanying this license ("Fonts") and associated 12 | documentation files (the "Font Software"), to reproduce and distribute the 13 | Font Software, including without limitation the rights to use, copy, merge, 14 | publish, distribute, and/or sell copies of the Font Software, and to permit 15 | persons to whom the Font Software is furnished to do so, subject to the 16 | following conditions: 17 | 18 | The above copyright and trademark notices and this permission notice shall 19 | be included in all copies of one or more of the Font Software typefaces. 20 | 21 | The Font Software may be modified, altered, or added to, and in particular 22 | the designs of glyphs or characters in the Fonts may be modified and 23 | additional glyphs or characters may be added to the Fonts, only if the fonts 24 | are renamed to names not containing either the words "Bitstream" or the word 25 | "Vera". 26 | 27 | This License becomes null and void to the extent applicable to Fonts or Font 28 | Software that has been modified and is distributed under the "Bitstream 29 | Vera" names. 30 | 31 | The Font Software may be sold as part of a larger software package but no 32 | copy of one or more of the Font Software typefaces may be sold by itself. 33 | 34 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 35 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, 36 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, 37 | TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME 38 | FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING 39 | ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 40 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF 41 | THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE 42 | FONT SOFTWARE. 43 | 44 | Except as contained in this notice, the names of Gnome, the Gnome 45 | Foundation, and Bitstream Inc., shall not be used in advertising or 46 | otherwise to promote the sale, use or other dealings in this Font Software 47 | without prior written authorization from the Gnome Foundation or Bitstream 48 | Inc., respectively. For further information, contact: fonts at gnome dot 49 | org. 50 | 51 | Arev Fonts Copyright 52 | ------------------------------ 53 | 54 | Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. 55 | 56 | Permission is hereby granted, free of charge, to any person obtaining 57 | a copy of the fonts accompanying this license ("Fonts") and 58 | associated documentation files (the "Font Software"), to reproduce 59 | and distribute the modifications to the Bitstream Vera Font Software, 60 | including without limitation the rights to use, copy, merge, publish, 61 | distribute, and/or sell copies of the Font Software, and to permit 62 | persons to whom the Font Software is furnished to do so, subject to 63 | the following conditions: 64 | 65 | The above copyright and trademark notices and this permission notice 66 | shall be included in all copies of one or more of the Font Software 67 | typefaces. 68 | 69 | The Font Software may be modified, altered, or added to, and in 70 | particular the designs of glyphs or characters in the Fonts may be 71 | modified and additional glyphs or characters may be added to the 72 | Fonts, only if the fonts are renamed to names not containing either 73 | the words "Tavmjong Bah" or the word "Arev". 74 | 75 | This License becomes null and void to the extent applicable to Fonts 76 | or Font Software that has been modified and is distributed under the 77 | "Tavmjong Bah Arev" names. 78 | 79 | The Font Software may be sold as part of a larger software package but 80 | no copy of one or more of the Font Software typefaces may be sold by 81 | itself. 82 | 83 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 84 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 85 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 86 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL 87 | TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 88 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 89 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 90 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 91 | OTHER DEALINGS IN THE FONT SOFTWARE. 92 | 93 | Except as contained in this notice, the name of Tavmjong Bah shall not 94 | be used in advertising or otherwise to promote the sale, use or other 95 | dealings in this Font Software without prior written authorization 96 | from Tavmjong Bah. For further information, contact: tavmjong @ free 97 | . fr. -------------------------------------------------------------------------------- /desktop/dejavu_font/DejaVuSansMono-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daa84/neovim-gtk/c03649276ee47caa9bc7beb68f6c800b8c97651a/desktop/dejavu_font/DejaVuSansMono-Bold.ttf -------------------------------------------------------------------------------- /desktop/dejavu_font/DejaVuSansMono-BoldOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daa84/neovim-gtk/c03649276ee47caa9bc7beb68f6c800b8c97651a/desktop/dejavu_font/DejaVuSansMono-BoldOblique.ttf -------------------------------------------------------------------------------- /desktop/dejavu_font/DejaVuSansMono-Oblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daa84/neovim-gtk/c03649276ee47caa9bc7beb68f6c800b8c97651a/desktop/dejavu_font/DejaVuSansMono-Oblique.ttf -------------------------------------------------------------------------------- /desktop/dejavu_font/DejaVuSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daa84/neovim-gtk/c03649276ee47caa9bc7beb68f6c800b8c97651a/desktop/dejavu_font/DejaVuSansMono.ttf -------------------------------------------------------------------------------- /desktop/org.daa.NeovimGtk-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 59 | 66 | 73 | 80 | 87 | 94 | 101 | 108 | 115 | 122 | 129 | 136 | 137 | Created with Sketch (http://www.bohemiancoding.com/sketch) 138 | 140 | 145 | 149 | 156 | 162 | 163 | 168 | 171 | 174 | 175 | 176 | 177 | 180 | 185 | 190 | 195 | 200 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /desktop/org.daa.NeovimGtk.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=NeovimGtk 3 | Comment=Gtk GUI for Neovim text editor 4 | Exec=nvim-gtk -- %F 5 | Icon=org.daa.NeovimGtk 6 | Type=Application 7 | Terminal=false 8 | Categories=GTK;Utility;TextEditor; 9 | StartupNotify=true 10 | MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++; 11 | -------------------------------------------------------------------------------- /desktop/org.daa.NeovimGtk.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 24 | 26 | 34 | 39 | 44 | 45 | 53 | 57 | 61 | 62 | 70 | 75 | 80 | 81 | 82 | 101 | 103 | 104 | 106 | image/svg+xml 107 | 109 | 110 | 111 | 112 | 113 | 118 | 124 | 129 | 133 | 139 | 146 | 152 | 158 | 159 | 160 | 161 | 162 | -------------------------------------------------------------------------------- /desktop/org.daa.NeovimGtk_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daa84/neovim-gtk/c03649276ee47caa9bc7beb68f6c800b8c97651a/desktop/org.daa.NeovimGtk_128.png -------------------------------------------------------------------------------- /desktop/org.daa.NeovimGtk_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daa84/neovim-gtk/c03649276ee47caa9bc7beb68f6c800b8c97651a/desktop/org.daa.NeovimGtk_48.png -------------------------------------------------------------------------------- /resources/neovim.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daa84/neovim-gtk/c03649276ee47caa9bc7beb68f6c800b8c97651a/resources/neovim.ico -------------------------------------------------------------------------------- /resources/side-panel.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | True 17 | False 18 | 19 | 20 | True 21 | False 22 | filebrowser.cd 23 | Go To Directory 24 | 25 | 26 | 27 | 28 | True 29 | False 30 | 31 | 32 | 33 | 34 | True 35 | False 36 | filebrowser.reload 37 | Reload 38 | 39 | 40 | 41 | 42 | True 43 | False 44 | Show Hidden Files 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 150 62 | False 63 | vertical 64 | 65 | 66 | False 67 | False 68 | 6 69 | dir_list_model 70 | 1 71 | 72 | 73 | 6 74 | 75 | 76 | 1 77 | 78 | 79 | 80 | 81 | 6 82 | end 83 | 84 | 85 | 0 86 | 87 | 88 | 89 | 90 | False 91 | True 92 | 0 93 | 94 | 95 | 96 | 97 | False 98 | 99 | 100 | False 101 | file_browser_tree_store 102 | False 103 | False 104 | 20 105 | True 106 | 107 | 108 | 109 | 110 | 111 | autosize 112 | 113 | 114 | 6 115 | 116 | 117 | 3 118 | 119 | 120 | 121 | 122 | 123 | 0 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | True 133 | True 134 | 1 135 | 136 | 137 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /runtime/plugin/nvim_gui_shim.vim: -------------------------------------------------------------------------------- 1 | " A Neovim plugin that implements GUI helper commands 2 | if !has('nvim') || exists('g:GuiLoaded') 3 | finish 4 | endif 5 | let g:GuiLoaded = 1 6 | 7 | if exists('g:GuiInternalClipboard') 8 | let s:LastRegType = 'v' 9 | function! provider#clipboard#Call(method, args) abort 10 | if a:method == 'get' 11 | return [rpcrequest(1, 'Gui', 'Clipboard', 'Get', a:args[0]), s:LastRegType] 12 | elseif a:method == 'set' 13 | let s:LastRegType = a:args[1] 14 | call rpcnotify(1, 'Gui', 'Clipboard', 'Set', a:args[2], join(a:args[0], ' ')) 15 | endif 16 | endfunction 17 | endif 18 | 19 | " Set GUI font 20 | function! GuiFont(fname, ...) abort 21 | call rpcnotify(1, 'Gui', 'Font', s:NvimQtToPangoFont(a:fname)) 22 | endfunction 23 | 24 | " Some subset of parse command from neovim-qt 25 | " to support interoperability 26 | function s:NvimQtToPangoFont(fname) 27 | let l:attrs = split(a:fname, ':') 28 | let l:size = -1 29 | for part in l:attrs 30 | if len(part) >= 2 && part[0] == 'h' 31 | let l:size = strpart(part, 1) 32 | endif 33 | endfor 34 | 35 | if l:size > 0 36 | return l:attrs[0] . ' ' . l:size 37 | endif 38 | 39 | return l:attrs[0] 40 | endf 41 | 42 | 43 | " The GuiFont command. For compatibility there is also Guifont 44 | function s:GuiFontCommand(fname, bang) abort 45 | if a:fname ==# '' 46 | if exists('g:GuiFont') 47 | echo g:GuiFont 48 | else 49 | echo 'No GuiFont is set' 50 | endif 51 | else 52 | call GuiFont(a:fname, a:bang ==# '!') 53 | endif 54 | endfunction 55 | command! -nargs=1 -bang Guifont call s:GuiFontCommand("", "") 56 | command! -nargs=1 -bang GuiFont call s:GuiFontCommand("", "") 57 | 58 | command! -nargs=? GuiFontFeatures call rpcnotify(1, 'Gui', 'FontFeatures', ) 59 | command! -nargs=1 GuiLinespace call rpcnotify(1, 'Gui', 'Linespace', ) 60 | 61 | command! NGToggleSidebar call rpcnotify(1, 'Gui', 'Command', 'ToggleSidebar') 62 | command! NGShowProjectView call rpcnotify(1, 'Gui', 'Command', 'ShowProjectView') 63 | command! -nargs=+ NGTransparency call rpcnotify(1, 'Gui', 'Command', 'Transparency', ) 64 | command! -nargs=1 NGPreferDarkTheme call rpcnotify(1, 'Gui', 'Command', 'PreferDarkTheme', ) 65 | command! -nargs=1 NGSetCursorBlink call rpcnotify(1, 'Gui', 'Command', 'SetCursorBlink', ) 66 | 67 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | use_try_shorthand = true 2 | -------------------------------------------------------------------------------- /screenshots/neovimgtk-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/daa84/neovim-gtk/c03649276ee47caa9bc7beb68f6c800b8c97651a/screenshots/neovimgtk-screen.png -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::borrow::Cow; 3 | 4 | use gdk; 5 | 6 | #[derive(Clone, PartialEq, Debug)] 7 | pub struct Color(pub f64, pub f64, pub f64); 8 | 9 | pub const COLOR_BLACK: Color = Color(0.0, 0.0, 0.0); 10 | pub const COLOR_WHITE: Color = Color(1.0, 1.0, 1.0); 11 | pub const COLOR_RED: Color = Color(1.0, 0.0, 0.0); 12 | 13 | impl<'a> From<&'a Color> for gdk::RGBA { 14 | fn from(color: &Color) -> Self { 15 | gdk::RGBA { 16 | red: color.0, 17 | green: color.1, 18 | blue: color.2, 19 | alpha: 1.0, 20 | } 21 | } 22 | } 23 | 24 | impl Color { 25 | pub fn from_cterm(idx: u8) -> Color { 26 | let color = TERMINAL_COLORS[usize::from(idx)]; 27 | Color( 28 | color.0 as f64 / 255.0, 29 | color.1 as f64 / 255.0, 30 | color.2 as f64 / 255.0, 31 | ) 32 | } 33 | 34 | pub fn from_indexed_color(indexed_color: u64) -> Color { 35 | let r = ((indexed_color >> 16) & 0xff) as f64; 36 | let g = ((indexed_color >> 8) & 0xff) as f64; 37 | let b = (indexed_color & 0xff) as f64; 38 | Color(r / 255.0, g / 255.0, b / 255.0) 39 | } 40 | 41 | pub fn to_u16(&self) -> (u16, u16, u16) { 42 | ( 43 | (std::u16::MAX as f64 * self.0) as u16, 44 | (std::u16::MAX as f64 * self.1) as u16, 45 | (std::u16::MAX as f64 * self.2) as u16, 46 | ) 47 | } 48 | 49 | pub fn to_hex(&self) -> String { 50 | format!( 51 | "#{:02X}{:02X}{:02X}", 52 | (self.0 * 255.0) as u8, 53 | (self.1 * 255.0) as u8, 54 | (self.2 * 255.0) as u8 55 | ) 56 | } 57 | 58 | pub fn inverse(&self, inverse_level: f64) -> Cow { 59 | debug_assert!(inverse_level >= 0.0 && inverse_level <= 1.0); 60 | 61 | if inverse_level <= 0.000001 { 62 | Cow::Borrowed(self) 63 | } else { 64 | Cow::Owned(Color( 65 | (inverse_level - self.0).abs(), 66 | (inverse_level - self.1).abs(), 67 | (inverse_level - self.2).abs(), 68 | )) 69 | } 70 | } 71 | } 72 | 73 | 74 | /// From https://jonasjacek.github.io/colors/ 75 | const TERMINAL_COLORS: [(u8, u8, u8); 256] = [ 76 | (0, 0, 0), 77 | (128, 0, 0), 78 | (0, 128, 0), 79 | (128, 128, 0), 80 | (0, 0, 128), 81 | (128, 0, 128), 82 | (0, 128, 128), 83 | (192, 192, 192), 84 | (128, 128, 128), 85 | (255, 0, 0), 86 | (0, 255, 0), 87 | (255, 255, 0), 88 | (0, 0, 255), 89 | (255, 0, 255), 90 | (0, 255, 255), 91 | (255, 255, 255), 92 | (0, 0, 0), 93 | (0, 0, 95), 94 | (0, 0, 135), 95 | (0, 0, 175), 96 | (0, 0, 215), 97 | (0, 0, 255), 98 | (0, 95, 0), 99 | (0, 95, 95), 100 | (0, 95, 135), 101 | (0, 95, 175), 102 | (0, 95, 215), 103 | (0, 95, 255), 104 | (0, 135, 0), 105 | (0, 135, 95), 106 | (0, 135, 135), 107 | (0, 135, 175), 108 | (0, 135, 215), 109 | (0, 135, 255), 110 | (0, 175, 0), 111 | (0, 175, 95), 112 | (0, 175, 135), 113 | (0, 175, 175), 114 | (0, 175, 215), 115 | (0, 175, 255), 116 | (0, 215, 0), 117 | (0, 215, 95), 118 | (0, 215, 135), 119 | (0, 215, 175), 120 | (0, 215, 215), 121 | (0, 215, 255), 122 | (0, 255, 0), 123 | (0, 255, 95), 124 | (0, 255, 135), 125 | (0, 255, 175), 126 | (0, 255, 215), 127 | (0, 255, 255), 128 | (95, 0, 0), 129 | (95, 0, 95), 130 | (95, 0, 135), 131 | (95, 0, 175), 132 | (95, 0, 215), 133 | (95, 0, 255), 134 | (95, 95, 0), 135 | (95, 95, 95), 136 | (95, 95, 135), 137 | (95, 95, 175), 138 | (95, 95, 215), 139 | (95, 95, 255), 140 | (95, 135, 0), 141 | (95, 135, 95), 142 | (95, 135, 135), 143 | (95, 135, 175), 144 | (95, 135, 215), 145 | (95, 135, 255), 146 | (95, 175, 0), 147 | (95, 175, 95), 148 | (95, 175, 135), 149 | (95, 175, 175), 150 | (95, 175, 215), 151 | (95, 175, 255), 152 | (95, 215, 0), 153 | (95, 215, 95), 154 | (95, 215, 135), 155 | (95, 215, 175), 156 | (95, 215, 215), 157 | (95, 215, 255), 158 | (95, 255, 0), 159 | (95, 255, 95), 160 | (95, 255, 135), 161 | (95, 255, 175), 162 | (95, 255, 215), 163 | (95, 255, 255), 164 | (135, 0, 0), 165 | (135, 0, 95), 166 | (135, 0, 135), 167 | (135, 0, 175), 168 | (135, 0, 215), 169 | (135, 0, 255), 170 | (135, 95, 0), 171 | (135, 95, 95), 172 | (135, 95, 135), 173 | (135, 95, 175), 174 | (135, 95, 215), 175 | (135, 95, 255), 176 | (135, 135, 0), 177 | (135, 135, 95), 178 | (135, 135, 135), 179 | (135, 135, 175), 180 | (135, 135, 215), 181 | (135, 135, 255), 182 | (135, 175, 0), 183 | (135, 175, 95), 184 | (135, 175, 135), 185 | (135, 175, 175), 186 | (135, 175, 215), 187 | (135, 175, 255), 188 | (135, 215, 0), 189 | (135, 215, 95), 190 | (135, 215, 135), 191 | (135, 215, 175), 192 | (135, 215, 215), 193 | (135, 215, 255), 194 | (135, 255, 0), 195 | (135, 255, 95), 196 | (135, 255, 135), 197 | (135, 255, 175), 198 | (135, 255, 215), 199 | (135, 255, 255), 200 | (175, 0, 0), 201 | (175, 0, 95), 202 | (175, 0, 135), 203 | (175, 0, 175), 204 | (175, 0, 215), 205 | (175, 0, 255), 206 | (175, 95, 0), 207 | (175, 95, 95), 208 | (175, 95, 135), 209 | (175, 95, 175), 210 | (175, 95, 215), 211 | (175, 95, 255), 212 | (175, 135, 0), 213 | (175, 135, 95), 214 | (175, 135, 135), 215 | (175, 135, 175), 216 | (175, 135, 215), 217 | (175, 135, 255), 218 | (175, 175, 0), 219 | (175, 175, 95), 220 | (175, 175, 135), 221 | (175, 175, 175), 222 | (175, 175, 215), 223 | (175, 175, 255), 224 | (175, 215, 0), 225 | (175, 215, 95), 226 | (175, 215, 135), 227 | (175, 215, 175), 228 | (175, 215, 215), 229 | (175, 215, 255), 230 | (175, 255, 0), 231 | (175, 255, 95), 232 | (175, 255, 135), 233 | (175, 255, 175), 234 | (175, 255, 215), 235 | (175, 255, 255), 236 | (215, 0, 0), 237 | (215, 0, 95), 238 | (215, 0, 135), 239 | (215, 0, 175), 240 | (215, 0, 215), 241 | (215, 0, 255), 242 | (215, 95, 0), 243 | (215, 95, 95), 244 | (215, 95, 135), 245 | (215, 95, 175), 246 | (215, 95, 215), 247 | (215, 95, 255), 248 | (215, 135, 0), 249 | (215, 135, 95), 250 | (215, 135, 135), 251 | (215, 135, 175), 252 | (215, 135, 215), 253 | (215, 135, 255), 254 | (215, 175, 0), 255 | (215, 175, 95), 256 | (215, 175, 135), 257 | (215, 175, 175), 258 | (215, 175, 215), 259 | (215, 175, 255), 260 | (215, 215, 0), 261 | (215, 215, 95), 262 | (215, 215, 135), 263 | (215, 215, 175), 264 | (215, 215, 215), 265 | (215, 215, 255), 266 | (215, 255, 0), 267 | (215, 255, 95), 268 | (215, 255, 135), 269 | (215, 255, 175), 270 | (215, 255, 215), 271 | (215, 255, 255), 272 | (255, 0, 0), 273 | (255, 0, 95), 274 | (255, 0, 135), 275 | (255, 0, 175), 276 | (255, 0, 215), 277 | (255, 0, 255), 278 | (255, 95, 0), 279 | (255, 95, 95), 280 | (255, 95, 135), 281 | (255, 95, 175), 282 | (255, 95, 215), 283 | (255, 95, 255), 284 | (255, 135, 0), 285 | (255, 135, 95), 286 | (255, 135, 135), 287 | (255, 135, 175), 288 | (255, 135, 215), 289 | (255, 135, 255), 290 | (255, 175, 0), 291 | (255, 175, 95), 292 | (255, 175, 135), 293 | (255, 175, 175), 294 | (255, 175, 215), 295 | (255, 175, 255), 296 | (255, 215, 0), 297 | (255, 215, 95), 298 | (255, 215, 135), 299 | (255, 215, 175), 300 | (255, 215, 215), 301 | (255, 215, 255), 302 | (255, 255, 0), 303 | (255, 255, 95), 304 | (255, 255, 135), 305 | (255, 255, 175), 306 | (255, 255, 215), 307 | (255, 255, 255), 308 | (8, 8, 8), 309 | (18, 18, 18), 310 | (28, 28, 28), 311 | (38, 38, 38), 312 | (48, 48, 48), 313 | (58, 58, 58), 314 | (68, 68, 68), 315 | (78, 78, 78), 316 | (88, 88, 88), 317 | (98, 98, 98), 318 | (108, 108, 108), 319 | (118, 118, 118), 320 | (128, 128, 128), 321 | (138, 138, 138), 322 | (148, 148, 148), 323 | (158, 158, 158), 324 | (168, 168, 168), 325 | (178, 178, 178), 326 | (188, 188, 188), 327 | (198, 198, 198), 328 | (208, 208, 208), 329 | (218, 218, 218), 330 | (228, 228, 228), 331 | (238, 238, 238), 332 | ]; 333 | 334 | #[cfg(test)] 335 | mod tests { 336 | use super::*; 337 | 338 | #[test] 339 | fn test_to_hex() { 340 | let col = Color(0.0, 1.0, 0.0); 341 | assert_eq!("#00FF00", &col.to_hex()); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/cursor.rs: -------------------------------------------------------------------------------- 1 | use cairo; 2 | use crate::mode; 3 | use crate::render; 4 | use crate::render::CellMetrics; 5 | use crate::highlight::HighlightMap; 6 | use std::sync::{Arc, Weak}; 7 | use crate::ui::UiMutex; 8 | 9 | use glib; 10 | 11 | struct Alpha(f64); 12 | 13 | impl Alpha { 14 | pub fn show(&mut self, step: f64) -> bool { 15 | self.0 += step; 16 | if self.0 > 1.0 { 17 | self.0 = 1.0; 18 | false 19 | } else { 20 | true 21 | } 22 | } 23 | pub fn hide(&mut self, step: f64) -> bool { 24 | self.0 -= step; 25 | if self.0 < 0.0 { 26 | self.0 = 0.0; 27 | false 28 | } else { 29 | true 30 | } 31 | } 32 | } 33 | 34 | #[derive(PartialEq)] 35 | enum AnimPhase { 36 | Shown, 37 | Hide, 38 | Hidden, 39 | Show, 40 | NoFocus, 41 | Busy, 42 | } 43 | 44 | struct BlinkCount { 45 | count: u32, 46 | max: u32, 47 | } 48 | 49 | impl BlinkCount { 50 | fn new(max: u32) -> Self { 51 | Self { count: 0, max } 52 | } 53 | } 54 | 55 | struct State { 56 | alpha: Alpha, 57 | anim_phase: AnimPhase, 58 | redraw_cb: Weak>, 59 | 60 | timer: Option, 61 | counter: Option, 62 | } 63 | 64 | impl State { 65 | fn new(redraw_cb: Weak>) -> Self { 66 | State { 67 | alpha: Alpha(1.0), 68 | anim_phase: AnimPhase::Shown, 69 | redraw_cb, 70 | timer: None, 71 | counter: None, 72 | } 73 | } 74 | 75 | fn reset_to(&mut self, phase: AnimPhase) { 76 | self.alpha = Alpha(1.0); 77 | self.anim_phase = phase; 78 | if let Some(timer_id) = self.timer.take() { 79 | glib::source_remove(timer_id); 80 | } 81 | } 82 | } 83 | 84 | pub trait Cursor { 85 | /// return cursor current alpha value 86 | fn draw( 87 | &self, 88 | ctx: &cairo::Context, 89 | font_ctx: &render::Context, 90 | line_y: f64, 91 | double_width: bool, 92 | hl: &HighlightMap, 93 | ) -> f64; 94 | 95 | fn is_visible(&self) -> bool; 96 | 97 | fn mode_info(&self) -> Option<&mode::ModeInfo>; 98 | } 99 | 100 | pub struct EmptyCursor; 101 | 102 | impl EmptyCursor { 103 | pub fn new() -> Self { 104 | EmptyCursor {} 105 | } 106 | } 107 | 108 | impl Cursor for EmptyCursor { 109 | fn draw( 110 | &self, 111 | _ctx: &cairo::Context, 112 | _font_ctx: &render::Context, 113 | _line_y: f64, 114 | _double_width: bool, 115 | _color: &HighlightMap, 116 | ) -> f64 { 117 | 0.0 118 | } 119 | 120 | fn is_visible(&self) -> bool { 121 | false 122 | } 123 | 124 | fn mode_info(&self) -> Option<&mode::ModeInfo> { 125 | None 126 | } 127 | } 128 | 129 | pub struct BlinkCursor { 130 | state: Arc>>, 131 | mode_info: Option, 132 | } 133 | 134 | impl BlinkCursor { 135 | pub fn new(redraw_cb: Weak>) -> Self { 136 | BlinkCursor { 137 | state: Arc::new(UiMutex::new(State::new(redraw_cb))), 138 | mode_info: None, 139 | } 140 | } 141 | 142 | pub fn set_mode_info(&mut self, mode_info: Option) { 143 | self.mode_info = mode_info; 144 | } 145 | 146 | pub fn set_cursor_blink(&mut self, val: i32) { 147 | let mut mut_state = self.state.borrow_mut(); 148 | mut_state.counter = if val < 0 { 149 | None 150 | } else { 151 | Some(BlinkCount::new(val as u32)) 152 | } 153 | } 154 | 155 | pub fn start(&mut self) { 156 | let blinkwait = self 157 | .mode_info 158 | .as_ref() 159 | .and_then(|mi| mi.blinkwait) 160 | .unwrap_or(500); 161 | 162 | let state = self.state.clone(); 163 | let mut mut_state = self.state.borrow_mut(); 164 | 165 | mut_state.reset_to(AnimPhase::Shown); 166 | 167 | if let Some(counter) = &mut mut_state.counter { 168 | counter.count = 0; 169 | } 170 | 171 | mut_state.timer = Some(glib::timeout_add( 172 | if blinkwait > 0 { blinkwait } else { 500 }, 173 | move || anim_step(&state), 174 | )); 175 | } 176 | 177 | pub fn reset_state(&mut self) { 178 | if self.state.borrow().anim_phase != AnimPhase::Busy { 179 | self.start(); 180 | } 181 | } 182 | 183 | pub fn enter_focus(&mut self) { 184 | if self.state.borrow().anim_phase != AnimPhase::Busy { 185 | self.start(); 186 | } 187 | } 188 | 189 | pub fn leave_focus(&mut self) { 190 | if self.state.borrow().anim_phase != AnimPhase::Busy { 191 | self.state.borrow_mut().reset_to(AnimPhase::NoFocus); 192 | } 193 | } 194 | 195 | pub fn busy_on(&mut self) { 196 | self.state.borrow_mut().reset_to(AnimPhase::Busy); 197 | } 198 | 199 | pub fn busy_off(&mut self) { 200 | self.start(); 201 | } 202 | } 203 | 204 | impl Cursor for BlinkCursor { 205 | fn draw( 206 | &self, 207 | ctx: &cairo::Context, 208 | font_ctx: &render::Context, 209 | line_y: f64, 210 | double_width: bool, 211 | hl: &HighlightMap, 212 | ) -> f64 { 213 | let state = self.state.borrow(); 214 | 215 | let current_point = ctx.get_current_point(); 216 | 217 | let bg = hl.cursor_bg(); 218 | ctx.set_source_rgba(bg.0, bg.1, bg.2, state.alpha.0); 219 | 220 | let (y, width, height) = cursor_rect( 221 | self.mode_info(), 222 | font_ctx.cell_metrics(), 223 | line_y, 224 | double_width, 225 | ); 226 | 227 | ctx.rectangle(current_point.0, y, width, height); 228 | if state.anim_phase == AnimPhase::NoFocus { 229 | ctx.stroke(); 230 | } else { 231 | ctx.fill(); 232 | } 233 | 234 | state.alpha.0 235 | } 236 | 237 | fn is_visible(&self) -> bool { 238 | let state = self.state.borrow(); 239 | 240 | if state.anim_phase == AnimPhase::Busy { 241 | return false; 242 | } 243 | 244 | if state.alpha.0 < 0.000001 { 245 | false 246 | } else { 247 | true 248 | } 249 | } 250 | 251 | fn mode_info(&self) -> Option<&mode::ModeInfo> { 252 | self.mode_info.as_ref() 253 | } 254 | } 255 | 256 | pub fn cursor_rect( 257 | mode_info: Option<&mode::ModeInfo>, 258 | cell_metrics: &CellMetrics, 259 | line_y: f64, 260 | double_width: bool, 261 | ) -> (f64, f64, f64) { 262 | let &CellMetrics { 263 | line_height, 264 | char_width, 265 | .. 266 | } = cell_metrics; 267 | 268 | if let Some(mode_info) = mode_info { 269 | match mode_info.cursor_shape() { 270 | None | Some(&mode::CursorShape::Unknown) | Some(&mode::CursorShape::Block) => { 271 | let cursor_width = if double_width { 272 | char_width * 2.0 273 | } else { 274 | char_width 275 | }; 276 | (line_y, cursor_width, line_height) 277 | } 278 | Some(&mode::CursorShape::Vertical) => { 279 | let cell_percentage = mode_info.cell_percentage(); 280 | let cursor_width = if cell_percentage > 0 { 281 | (char_width * cell_percentage as f64) / 100.0 282 | } else { 283 | char_width 284 | }; 285 | (line_y, cursor_width, line_height) 286 | } 287 | Some(&mode::CursorShape::Horizontal) => { 288 | let cell_percentage = mode_info.cell_percentage(); 289 | let cursor_width = if double_width { 290 | char_width * 2.0 291 | } else { 292 | char_width 293 | }; 294 | 295 | if cell_percentage > 0 { 296 | let height = (line_height * cell_percentage as f64) / 100.0; 297 | (line_y + line_height - height, cursor_width, height) 298 | } else { 299 | (line_y, cursor_width, line_height) 300 | } 301 | } 302 | } 303 | } else { 304 | let cursor_width = if double_width { 305 | char_width * 2.0 306 | } else { 307 | char_width 308 | }; 309 | 310 | (line_y, cursor_width, line_height) 311 | } 312 | } 313 | 314 | fn anim_step(state: &Arc>>) -> glib::Continue { 315 | let mut mut_state = state.borrow_mut(); 316 | 317 | let next_event = match mut_state.anim_phase { 318 | AnimPhase::Shown => { 319 | if let Some(counter) = &mut mut_state.counter { 320 | if counter.count < counter.max { 321 | counter.count += 1; 322 | mut_state.anim_phase = AnimPhase::Hide; 323 | Some(60) 324 | } else { 325 | None 326 | } 327 | } else { 328 | mut_state.anim_phase = AnimPhase::Hide; 329 | Some(60) 330 | } 331 | } 332 | AnimPhase::Hide => { 333 | if !mut_state.alpha.hide(0.3) { 334 | mut_state.anim_phase = AnimPhase::Hidden; 335 | 336 | Some(300) 337 | } else { 338 | None 339 | } 340 | } 341 | AnimPhase::Hidden => { 342 | mut_state.anim_phase = AnimPhase::Show; 343 | 344 | Some(60) 345 | } 346 | AnimPhase::Show => { 347 | if !mut_state.alpha.show(0.3) { 348 | mut_state.anim_phase = AnimPhase::Shown; 349 | 350 | Some(500) 351 | } else { 352 | None 353 | } 354 | } 355 | AnimPhase::NoFocus => None, 356 | AnimPhase::Busy => None, 357 | }; 358 | 359 | let redraw_cb = mut_state.redraw_cb.upgrade().unwrap(); 360 | let mut redraw_cb = redraw_cb.borrow_mut(); 361 | redraw_cb.queue_redraw_cursor(); 362 | 363 | if let Some(timeout) = next_event { 364 | let moved_state = state.clone(); 365 | mut_state.timer = Some(glib::timeout_add(timeout, move || anim_step(&moved_state))); 366 | 367 | glib::Continue(false) 368 | } else { 369 | glib::Continue(true) 370 | } 371 | } 372 | 373 | impl Drop for BlinkCursor { 374 | fn drop(&mut self) { 375 | if let Some(timer_id) = self.state.borrow_mut().timer.take() { 376 | glib::source_remove(timer_id); 377 | } 378 | } 379 | } 380 | 381 | pub trait CursorRedrawCb { 382 | fn queue_redraw_cursor(&mut self); 383 | } 384 | 385 | #[cfg(test)] 386 | mod tests { 387 | use super::*; 388 | use std::collections::HashMap; 389 | 390 | #[test] 391 | fn test_cursor_rect_horizontal() { 392 | let mut mode_data = HashMap::new(); 393 | mode_data.insert("cursor_shape".to_owned(), From::from("horizontal")); 394 | mode_data.insert("cell_percentage".to_owned(), From::from(25)); 395 | 396 | let mode_info = mode::ModeInfo::new(&mode_data).ok(); 397 | let char_width = 50.0; 398 | let line_height = 30.0; 399 | let line_y = 0.0; 400 | 401 | let (y, width, height) = cursor_rect( 402 | mode_info.as_ref(), 403 | &CellMetrics::new_hw(line_height, char_width), 404 | line_y, 405 | false, 406 | ); 407 | assert_eq!(line_y + line_height - line_height / 4.0, y); 408 | assert_eq!(char_width, width); 409 | assert_eq!(line_height / 4.0, height); 410 | } 411 | 412 | #[test] 413 | fn test_cursor_rect_horizontal_doublewidth() { 414 | let mut mode_data = HashMap::new(); 415 | mode_data.insert("cursor_shape".to_owned(), From::from("horizontal")); 416 | mode_data.insert("cell_percentage".to_owned(), From::from(25)); 417 | 418 | let mode_info = mode::ModeInfo::new(&mode_data).ok(); 419 | let char_width = 50.0; 420 | let line_height = 30.0; 421 | let line_y = 0.0; 422 | 423 | let (y, width, height) = cursor_rect( 424 | mode_info.as_ref(), 425 | &CellMetrics::new_hw(line_height, char_width), 426 | line_y, 427 | true, 428 | ); 429 | assert_eq!(line_y + line_height - line_height / 4.0, y); 430 | assert_eq!(char_width * 2.0, width); 431 | assert_eq!(line_height / 4.0, height); 432 | } 433 | 434 | #[test] 435 | fn test_cursor_rect_vertical() { 436 | let mut mode_data = HashMap::new(); 437 | mode_data.insert("cursor_shape".to_owned(), From::from("vertical")); 438 | mode_data.insert("cell_percentage".to_owned(), From::from(25)); 439 | 440 | let mode_info = mode::ModeInfo::new(&mode_data).ok(); 441 | let char_width = 50.0; 442 | let line_height = 30.0; 443 | let line_y = 0.0; 444 | 445 | let (y, width, height) = cursor_rect( 446 | mode_info.as_ref(), 447 | &CellMetrics::new_hw(line_height, char_width), 448 | line_y, 449 | false, 450 | ); 451 | assert_eq!(line_y, y); 452 | assert_eq!(char_width / 4.0, width); 453 | assert_eq!(line_height, height); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /src/dirs.rs: -------------------------------------------------------------------------------- 1 | use std; 2 | use std::path::PathBuf; 3 | use crate::env_dirs; 4 | 5 | pub fn get_app_config_dir_create() -> Result { 6 | let config_dir = get_app_config_dir()?; 7 | 8 | std::fs::create_dir_all(&config_dir).map_err( 9 | |e| format!("{}", e), 10 | )?; 11 | 12 | Ok(config_dir) 13 | } 14 | 15 | pub fn get_app_config_dir() -> Result { 16 | let mut config_dir = get_xdg_config_dir()?; 17 | 18 | config_dir.push("nvim-gtk"); 19 | 20 | Ok(config_dir) 21 | } 22 | 23 | fn get_xdg_config_dir() -> Result { 24 | if let Ok(config_path) = std::env::var("XDG_CONFIG_HOME") { 25 | return Ok(PathBuf::from(config_path)); 26 | } 27 | 28 | let mut home_dir = env_dirs::home_dir().ok_or( 29 | "Impossible to get your home dir!", 30 | )?; 31 | home_dir.push(".config"); 32 | Ok(home_dir) 33 | } 34 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use htmlescape::encode_minimal; 4 | 5 | use gtk; 6 | use gtk::prelude::*; 7 | 8 | use crate::shell; 9 | 10 | pub struct ErrorArea { 11 | base: gtk::Box, 12 | label: gtk::Label, 13 | } 14 | 15 | impl ErrorArea { 16 | pub fn new() -> Self { 17 | let base = gtk::Box::new(gtk::Orientation::Horizontal, 0); 18 | 19 | let label = gtk::Label::new(None); 20 | label.set_line_wrap(true); 21 | let error_image = gtk::Image::new_from_icon_name(Some("dialog-error"), gtk::IconSize::Dialog); 22 | base.pack_start(&error_image, false, true, 10); 23 | base.pack_start(&label, true, true, 1); 24 | 25 | ErrorArea { base, label } 26 | } 27 | 28 | pub fn show_nvim_init_error(&self, err: &str) { 29 | error!("Can't initialize nvim: {}", err); 30 | self.label.set_markup(&format!( 31 | "Can't initialize nvim:\n\ 32 | {}\n\n\ 33 | Possible error reasons:\n\ 34 | ● Not supported nvim version (minimum supported version is {})\n\ 35 | ● Error in configuration file (init.vim or ginit.vim)", 36 | encode_minimal(err), 37 | shell::MINIMUM_SUPPORTED_NVIM_VERSION 38 | )); 39 | self.base.show_all(); 40 | } 41 | 42 | pub fn show_nvim_start_error(&self, err: &str, cmd: &str) { 43 | error!("Can't start nvim: {}\nCommand line: {}", err, cmd); 44 | self.label.set_markup(&format!( 45 | "Can't start nvim instance:\n\ 46 | {}\n\ 47 | {}\n\n\ 48 | Possible error reasons:\n\ 49 | ● Not supported nvim version (minimum supported version is {})\n\ 50 | ● Error in configuration file (init.vim or ginit.vim)\n\ 51 | ● Wrong nvim binary path \ 52 | (right path can be passed with --nvim-bin-path=path_here)", 53 | encode_minimal(cmd), 54 | encode_minimal(err), 55 | shell::MINIMUM_SUPPORTED_NVIM_VERSION 56 | )); 57 | self.base.show_all(); 58 | } 59 | } 60 | 61 | impl Deref for ErrorArea { 62 | type Target = gtk::Box; 63 | 64 | fn deref(&self) -> >k::Box { 65 | &self.base 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/grid.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Index, IndexMut}; 2 | use std::rc::Rc; 3 | 4 | use fnv::FnvHashMap; 5 | 6 | use neovim_lib::Value; 7 | 8 | use crate::highlight::{Highlight, HighlightMap}; 9 | use crate::ui_model::{ModelRect, ModelRectVec, UiModel}; 10 | 11 | const DEFAULT_GRID: u64 = 1; 12 | 13 | pub struct GridMap { 14 | grids: FnvHashMap, 15 | } 16 | 17 | impl Index for GridMap { 18 | type Output = Grid; 19 | 20 | fn index(&self, idx: u64) -> &Grid { 21 | &self.grids[&idx] 22 | } 23 | } 24 | 25 | impl IndexMut for GridMap { 26 | fn index_mut(&mut self, idx: u64) -> &mut Grid { 27 | self.grids.get_mut(&idx).unwrap() 28 | } 29 | } 30 | 31 | impl GridMap { 32 | pub fn new() -> Self { 33 | GridMap { 34 | grids: FnvHashMap::default(), 35 | } 36 | } 37 | 38 | pub fn current(&self) -> Option<&Grid> { 39 | self.grids.get(&DEFAULT_GRID) 40 | } 41 | 42 | pub fn current_model_mut(&mut self) -> Option<&mut UiModel> { 43 | self.grids.get_mut(&DEFAULT_GRID).map(|g| &mut g.model) 44 | } 45 | 46 | pub fn current_model(&self) -> Option<&UiModel> { 47 | self.grids.get(&DEFAULT_GRID).map(|g| &g.model) 48 | } 49 | 50 | pub fn get_or_create(&mut self, idx: u64) -> &mut Grid { 51 | if self.grids.contains_key(&idx) { 52 | return self.grids.get_mut(&idx).unwrap(); 53 | } 54 | 55 | self.grids.insert(idx, Grid::new()); 56 | self.grids.get_mut(&idx).unwrap() 57 | } 58 | 59 | pub fn destroy(&mut self, idx: u64) { 60 | self.grids.remove(&idx); 61 | } 62 | 63 | pub fn clear_glyphs(&mut self) { 64 | for grid in self.grids.values_mut() { 65 | grid.model.clear_glyphs(); 66 | } 67 | } 68 | } 69 | 70 | pub struct Grid { 71 | model: UiModel, 72 | } 73 | 74 | impl Grid { 75 | pub fn new() -> Self { 76 | Grid { 77 | model: UiModel::empty(), 78 | } 79 | } 80 | 81 | pub fn get_cursor(&self) -> (usize, usize) { 82 | self.model.get_cursor() 83 | } 84 | 85 | pub fn cur_point(&self) -> ModelRect { 86 | self.model.cur_point() 87 | } 88 | 89 | pub fn resize(&mut self, columns: u64, rows: u64) { 90 | if self.model.columns != columns as usize || self.model.rows != rows as usize { 91 | self.model = UiModel::new(rows, columns); 92 | } 93 | } 94 | 95 | pub fn cursor_goto(&mut self, row: usize, col: usize) -> ModelRectVec { 96 | self.model.set_cursor(row, col) 97 | } 98 | 99 | pub fn clear(&mut self, default_hl: &Rc) { 100 | self.model.clear(default_hl); 101 | } 102 | 103 | pub fn line( 104 | &mut self, 105 | row: usize, 106 | col_start: usize, 107 | cells: Vec>, 108 | highlights: &HighlightMap, 109 | ) -> ModelRect { 110 | let mut hl_id = None; 111 | let mut col_end = col_start; 112 | 113 | for cell in cells { 114 | let ch = cell.get(0).unwrap().as_str().unwrap_or(""); 115 | hl_id = cell.get(1).and_then(|h| h.as_u64()).or(hl_id); 116 | let repeat = cell.get(2).and_then(|r| r.as_u64()).unwrap_or(1) as usize; 117 | 118 | self.model.put( 119 | row, 120 | col_end, 121 | ch, 122 | ch.is_empty(), 123 | repeat, 124 | highlights.get(hl_id), 125 | ); 126 | col_end += repeat; 127 | } 128 | 129 | ModelRect::new(row, row, col_start, col_end - 1) 130 | } 131 | 132 | pub fn scroll( 133 | &mut self, 134 | top: u64, 135 | bot: u64, 136 | left: u64, 137 | right: u64, 138 | rows: i64, 139 | _: i64, 140 | default_hl: &Rc, 141 | ) -> ModelRect { 142 | self.model.scroll( 143 | top as i64, 144 | bot as i64 - 1, 145 | left as usize, 146 | right as usize - 1, 147 | rows, 148 | default_hl, 149 | ) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/highlight.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::rc::Rc; 3 | 4 | use fnv::FnvHashMap; 5 | 6 | use crate::color::*; 7 | use crate::ui_model::Cell; 8 | use neovim_lib::Value; 9 | 10 | pub struct HighlightMap { 11 | highlights: FnvHashMap>, 12 | default_hl: Rc, 13 | bg_color: Color, 14 | fg_color: Color, 15 | sp_color: Color, 16 | 17 | cterm_bg_color: Color, 18 | cterm_fg_color: Color, 19 | cterm_color: bool, 20 | 21 | pmenu: Rc, 22 | pmenu_sel: Rc, 23 | cursor: Rc, 24 | } 25 | 26 | impl HighlightMap { 27 | pub fn new() -> Self { 28 | let default_hl = Rc::new(Highlight::new()); 29 | HighlightMap { 30 | highlights: FnvHashMap::default(), 31 | bg_color: COLOR_BLACK, 32 | fg_color: COLOR_WHITE, 33 | sp_color: COLOR_RED, 34 | 35 | cterm_bg_color: COLOR_BLACK, 36 | cterm_fg_color: COLOR_WHITE, 37 | cterm_color: false, 38 | 39 | pmenu: default_hl.clone(), 40 | pmenu_sel: default_hl.clone(), 41 | cursor: default_hl.clone(), 42 | 43 | default_hl, 44 | } 45 | } 46 | 47 | pub fn default_hl(&self) -> Rc { 48 | self.default_hl.clone() 49 | } 50 | 51 | pub fn set_defaults( 52 | &mut self, 53 | fg: Color, 54 | bg: Color, 55 | sp: Color, 56 | cterm_fg: Color, 57 | cterm_bg: Color, 58 | ) { 59 | self.fg_color = fg; 60 | self.bg_color = bg; 61 | self.sp_color = sp; 62 | self.cterm_fg_color = cterm_fg; 63 | self.cterm_bg_color = cterm_bg; 64 | } 65 | 66 | pub fn set_use_cterm(&mut self, cterm_color: bool) { 67 | self.cterm_color = cterm_color; 68 | } 69 | 70 | pub fn bg(&self) -> &Color { 71 | if self.cterm_color { 72 | &self.cterm_bg_color 73 | } else { 74 | &self.bg_color 75 | } 76 | } 77 | 78 | pub fn fg(&self) -> &Color { 79 | if self.cterm_color { 80 | &self.cterm_fg_color 81 | } else { 82 | &self.fg_color 83 | } 84 | } 85 | 86 | pub fn get(&self, idx: Option) -> Rc { 87 | idx.and_then(|idx| self.highlights.get(&idx)) 88 | .map(Rc::clone) 89 | .unwrap_or_else(|| { 90 | self.highlights 91 | .get(&0) 92 | .map(Rc::clone) 93 | .unwrap_or_else(|| self.default_hl.clone()) 94 | }) 95 | } 96 | 97 | pub fn set(&mut self, idx: u64, hl: &HashMap, info: &[HashMap]) { 98 | let hl = Rc::new(Highlight::from_value_map(&hl)); 99 | 100 | for item in info { 101 | match item.get("hi_name").and_then(Value::as_str) { 102 | Some("Pmenu") => self.pmenu = hl.clone(), 103 | Some("PmenuSel") => self.pmenu_sel = hl.clone(), 104 | Some("Cursor") => self.cursor = hl.clone(), 105 | _ => (), 106 | } 107 | } 108 | 109 | self.highlights.insert(idx, hl); 110 | } 111 | 112 | pub fn cell_fg<'a>(&'a self, cell: &'a Cell) -> Option<&'a Color> { 113 | if !cell.hl.reverse { 114 | cell.hl.foreground.as_ref() 115 | } else { 116 | cell.hl.background.as_ref().or_else(|| Some(self.bg())) 117 | } 118 | } 119 | 120 | pub fn actual_cell_fg<'a>(&'a self, cell: &'a Cell) -> &'a Color { 121 | if !cell.hl.reverse { 122 | cell.hl.foreground.as_ref().unwrap_or_else(|| self.fg()) 123 | } else { 124 | cell.hl.background.as_ref().unwrap_or_else(|| self.bg()) 125 | } 126 | } 127 | 128 | pub fn cell_bg<'a>(&'a self, cell: &'a Cell) -> Option<&'a Color> { 129 | if !cell.hl.reverse { 130 | cell.hl.background.as_ref() 131 | } else { 132 | cell.hl.foreground.as_ref().or_else(|| Some(self.fg())) 133 | } 134 | } 135 | 136 | #[inline] 137 | pub fn actual_cell_sp<'a>(&'a self, cell: &'a Cell) -> &'a Color { 138 | cell.hl.special.as_ref().unwrap_or(&self.sp_color) 139 | } 140 | 141 | pub fn pmenu_bg(&self) -> &Color { 142 | if !self.pmenu.reverse { 143 | self.pmenu.background.as_ref().unwrap_or_else(|| self.bg()) 144 | } else { 145 | self.pmenu.foreground.as_ref().unwrap_or_else(|| self.fg()) 146 | } 147 | } 148 | 149 | pub fn pmenu_fg(&self) -> &Color { 150 | if !self.pmenu.reverse { 151 | self.pmenu.foreground.as_ref().unwrap_or_else(|| self.fg()) 152 | } else { 153 | self.pmenu.background.as_ref().unwrap_or_else(|| self.bg()) 154 | } 155 | } 156 | 157 | pub fn pmenu_bg_sel(&self) -> &Color { 158 | if !self.pmenu_sel.reverse { 159 | self.pmenu_sel 160 | .background 161 | .as_ref() 162 | .unwrap_or_else(|| self.bg()) 163 | } else { 164 | self.pmenu_sel 165 | .foreground 166 | .as_ref() 167 | .unwrap_or_else(|| self.fg()) 168 | } 169 | } 170 | 171 | pub fn pmenu_fg_sel(&self) -> &Color { 172 | if !self.pmenu_sel.reverse { 173 | self.pmenu_sel 174 | .foreground 175 | .as_ref() 176 | .unwrap_or_else(|| self.fg()) 177 | } else { 178 | self.pmenu_sel 179 | .background 180 | .as_ref() 181 | .unwrap_or_else(|| self.bg()) 182 | } 183 | } 184 | 185 | pub fn cursor_bg(&self) -> &Color { 186 | if !self.cursor.reverse { 187 | self.cursor.background.as_ref().unwrap_or_else(|| self.bg()) 188 | } else { 189 | self.cursor.foreground.as_ref().unwrap_or_else(|| self.fg()) 190 | } 191 | } 192 | } 193 | 194 | #[derive(Clone)] 195 | pub struct Highlight { 196 | pub italic: bool, 197 | pub bold: bool, 198 | pub underline: bool, 199 | pub undercurl: bool, 200 | pub strikethrough: bool, 201 | pub foreground: Option, 202 | pub background: Option, 203 | pub special: Option, 204 | pub reverse: bool, 205 | } 206 | 207 | impl Highlight { 208 | pub fn new() -> Self { 209 | Highlight { 210 | foreground: None, 211 | background: None, 212 | special: None, 213 | italic: false, 214 | bold: false, 215 | underline: false, 216 | undercurl: false, 217 | strikethrough: false, 218 | reverse: false, 219 | } 220 | } 221 | 222 | pub fn from_value_map(attrs: &HashMap) -> Self { 223 | let mut model_attrs = Highlight::new(); 224 | 225 | for (ref key, ref val) in attrs { 226 | match key.as_ref() { 227 | "foreground" => { 228 | if let Some(fg) = val.as_u64() { 229 | model_attrs.foreground = Some(Color::from_indexed_color(fg)); 230 | } 231 | } 232 | "background" => { 233 | if let Some(bg) = val.as_u64() { 234 | model_attrs.background = Some(Color::from_indexed_color(bg)); 235 | } 236 | } 237 | "special" => { 238 | if let Some(bg) = val.as_u64() { 239 | model_attrs.special = Some(Color::from_indexed_color(bg)); 240 | } 241 | } 242 | "reverse" => model_attrs.reverse = true, 243 | "bold" => model_attrs.bold = true, 244 | "italic" => model_attrs.italic = true, 245 | "underline" => model_attrs.underline = true, 246 | "undercurl" => model_attrs.undercurl = true, 247 | "strikethrough" => model_attrs.strikethrough = true, 248 | attr_key => error!("unknown attribute {}", attr_key), 249 | }; 250 | } 251 | 252 | model_attrs 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | 2 | use gtk::prelude::*; 3 | use gdk; 4 | use gdk::EventKey; 5 | use phf; 6 | use neovim_lib::{Neovim, NeovimApi}; 7 | 8 | include!(concat!(env!("OUT_DIR"), "/key_map_table.rs")); 9 | 10 | 11 | pub fn keyval_to_input_string(in_str: &str, in_state: gdk::ModifierType) -> String { 12 | let mut val = in_str; 13 | let mut state = in_state; 14 | let mut input = String::new(); 15 | 16 | debug!("keyval -> {}", in_str); 17 | 18 | // CTRL-^ and CTRL-@ don't work in the normal way. 19 | if state.contains(gdk::ModifierType::CONTROL_MASK) && !state.contains(gdk::ModifierType::SHIFT_MASK) && 20 | !state.contains(gdk::ModifierType::MOD1_MASK) 21 | { 22 | if val == "6" { 23 | val = "^"; 24 | } else if val == "2" { 25 | val = "@"; 26 | } 27 | } 28 | 29 | let chars: Vec = in_str.chars().collect(); 30 | 31 | if chars.len() == 1 { 32 | let ch = chars[0]; 33 | 34 | // Remove SHIFT 35 | if ch.is_ascii() && !ch.is_alphanumeric() { 36 | state.remove(gdk::ModifierType::SHIFT_MASK); 37 | } 38 | } 39 | 40 | if val == "<" { 41 | val = "lt"; 42 | } 43 | 44 | if state.contains(gdk::ModifierType::SHIFT_MASK) { 45 | input.push_str("S-"); 46 | } 47 | if state.contains(gdk::ModifierType::CONTROL_MASK) { 48 | input.push_str("C-"); 49 | } 50 | if state.contains(gdk::ModifierType::MOD1_MASK) { 51 | input.push_str("A-"); 52 | } 53 | 54 | input.push_str(val); 55 | 56 | if input.chars().count() > 1 { 57 | format!("<{}>", input) 58 | } else { 59 | input 60 | } 61 | } 62 | 63 | pub fn convert_key(ev: &EventKey) -> Option { 64 | let keyval = ev.get_keyval(); 65 | let state = ev.get_state(); 66 | if let Some(ref keyval_name) = gdk::keyval_name(keyval) { 67 | if let Some(cnvt) = KEYVAL_MAP.get(keyval_name as &str).cloned() { 68 | return Some(keyval_to_input_string(cnvt, state)); 69 | } 70 | } 71 | 72 | if let Some(ch) = gdk::keyval_to_unicode(keyval) { 73 | Some(keyval_to_input_string(&ch.to_string(), state)) 74 | } else { 75 | None 76 | } 77 | } 78 | 79 | pub fn im_input(nvim: &mut Neovim, input: &str) { 80 | debug!("nvim_input -> {}", input); 81 | 82 | let input: String = input 83 | .chars() 84 | .map(|ch| { 85 | keyval_to_input_string(&ch.to_string(), gdk::ModifierType::empty()) 86 | }) 87 | .collect(); 88 | nvim.input(&input).expect("Error run input command to nvim"); 89 | } 90 | 91 | pub fn gtk_key_press(nvim: &mut Neovim, ev: &EventKey) -> Inhibit { 92 | if let Some(input) = convert_key(ev) { 93 | debug!("nvim_input -> {}", input); 94 | nvim.input(&input).expect("Error run input command to nvim"); 95 | Inhibit(true) 96 | } else { 97 | Inhibit(false) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![windows_subsystem = "windows"] 2 | 3 | extern crate dirs as env_dirs; 4 | extern crate glib_sys as glib_ffi; 5 | extern crate gobject_sys as gobject_ffi; 6 | #[macro_use] 7 | extern crate log; 8 | #[macro_use] 9 | extern crate serde_derive; 10 | 11 | mod sys; 12 | 13 | mod color; 14 | mod dirs; 15 | mod mode; 16 | mod nvim_config; 17 | mod ui_model; 18 | mod value; 19 | #[macro_use] 20 | mod ui; 21 | mod cmd_line; 22 | mod cursor; 23 | mod error; 24 | mod file_browser; 25 | mod grid; 26 | mod highlight; 27 | mod input; 28 | mod misc; 29 | mod nvim; 30 | mod plug_manager; 31 | mod popup_menu; 32 | mod project; 33 | mod render; 34 | mod settings; 35 | mod shell; 36 | mod shell_dlg; 37 | mod subscriptions; 38 | mod tabline; 39 | 40 | use gio::prelude::*; 41 | use std::cell::RefCell; 42 | use std::io::Read; 43 | #[cfg(unix)] 44 | use unix_daemonize::{daemonize_redirect, ChdirMode}; 45 | 46 | use crate::ui::Ui; 47 | use crate::shell::ShellOptions; 48 | 49 | use clap::{App, Arg, ArgMatches}; 50 | 51 | include!(concat!(env!("OUT_DIR"), "/version.rs")); 52 | 53 | fn main() { 54 | env_logger::init(); 55 | 56 | let matches = App::new("NeovimGtk") 57 | .version(GIT_BUILD_VERSION.unwrap_or(env!("CARGO_PKG_VERSION"))) 58 | .author(env!("CARGO_PKG_AUTHORS")) 59 | .about(misc::about_comments().as_str()) 60 | .arg(Arg::with_name("no-fork") 61 | .long("no-fork") 62 | .help("Prevent detach from console")) 63 | .arg(Arg::with_name("disable-win-restore") 64 | .long("disable-win-restore") 65 | .help("Don't restore window size at start")) 66 | .arg(Arg::with_name("timeout") 67 | .long("timeout") 68 | .default_value("10") 69 | .help("Wait timeout in seconds. If nvim does not response in given time NvimGtk stops") 70 | .takes_value(true)) 71 | .arg(Arg::with_name("cterm-colors") 72 | .long("cterm-colors") 73 | .help("Use ctermfg/ctermbg instead of guifg/guibg")) 74 | .arg(Arg::with_name("files").help("Files to open").multiple(true)) 75 | .arg( 76 | Arg::with_name("nvim-bin-path") 77 | .long("nvim-bin-path") 78 | .help("Path to nvim binary") 79 | .takes_value(true), 80 | ).arg( 81 | Arg::with_name("nvim-args") 82 | .help("Args will be passed to nvim") 83 | .last(true) 84 | .multiple(true), 85 | ).get_matches(); 86 | 87 | let input_data = RefCell::new(read_piped_input()); 88 | 89 | #[cfg(unix)] 90 | { 91 | // fork to background by default 92 | if !matches.is_present("no-fork") { 93 | daemonize_redirect( 94 | Some(format!("/tmp/nvim-gtk_stdout.{}.log", whoami::username())), 95 | Some(format!("/tmp/nvim-gtk_stderr.{}.log", whoami::username())), 96 | ChdirMode::NoChdir, 97 | ) 98 | .unwrap(); 99 | } 100 | } 101 | 102 | let app_flags = gio::ApplicationFlags::HANDLES_OPEN | gio::ApplicationFlags::NON_UNIQUE; 103 | 104 | glib::set_program_name(Some("NeovimGtk")); 105 | 106 | let app = if cfg!(debug_assertions) { 107 | gtk::Application::new(Some("org.daa.NeovimGtkDebug"), app_flags) 108 | } else { 109 | gtk::Application::new(Some("org.daa.NeovimGtk"), app_flags) 110 | } 111 | .expect("Failed to initialize GTK application"); 112 | 113 | let matches_copy = matches.clone(); 114 | app.connect_activate(move |app| { 115 | let input_data = input_data 116 | .replace(None) 117 | .filter(|_input| !matches_copy.is_present("files")); 118 | 119 | activate(app, &matches_copy, input_data) 120 | }); 121 | 122 | let matches_copy = matches.clone(); 123 | app.connect_open(move |app, files, _| open(app, files, &matches_copy)); 124 | 125 | let app_ref = app.clone(); 126 | let matches_copy = matches.clone(); 127 | let new_window_action = gio::SimpleAction::new("new-window", None); 128 | new_window_action.connect_activate(move |_, _| activate(&app_ref, &matches_copy, None)); 129 | app.add_action(&new_window_action); 130 | 131 | gtk::Window::set_default_icon_name("org.daa.NeovimGtk"); 132 | 133 | let app_exe = std::env::args().next().unwrap_or_else(|| "nvim-gtk".to_owned()); 134 | 135 | app.run( 136 | &std::iter::once(app_exe) 137 | .chain( 138 | matches 139 | .values_of("files") 140 | .unwrap_or_default() 141 | .map(str::to_owned), 142 | ) 143 | .collect::>(), 144 | ); 145 | } 146 | 147 | fn open(app: >k::Application, files: &[gio::File], matches: &ArgMatches) { 148 | let files_list: Vec = files 149 | .iter() 150 | .filter_map(|f| f.get_path()?.to_str().map(str::to_owned)) 151 | .collect(); 152 | 153 | let mut ui = Ui::new( 154 | ShellOptions::new(matches, None), 155 | files_list.into_boxed_slice(), 156 | ); 157 | 158 | ui.init(app, !matches.is_present("disable-win-restore")); 159 | } 160 | 161 | fn activate(app: >k::Application, matches: &ArgMatches, input_data: Option) { 162 | let mut ui = Ui::new(ShellOptions::new(matches, input_data), Box::new([])); 163 | 164 | ui.init(app, !matches.is_present("disable-win-restore")); 165 | } 166 | 167 | fn read_piped_input() -> Option { 168 | if atty::isnt(atty::Stream::Stdin) { 169 | let mut buf = String::new(); 170 | match std::io::stdin().read_to_string(&mut buf) { 171 | Ok(size) if size > 0 => Some(buf), 172 | Ok(_) => None, 173 | Err(err) => { 174 | error!("Error read stdin {}", err); 175 | None 176 | } 177 | } 178 | } else { 179 | None 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/misc.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::mem; 3 | 4 | use lazy_static::lazy_static; 5 | 6 | use percent_encoding::percent_decode; 7 | use regex::Regex; 8 | 9 | use crate::shell; 10 | 11 | /// Split comma separated parameters with ',' except escaped '\\,' 12 | pub fn split_at_comma(source: &str) -> Vec { 13 | let mut items = Vec::new(); 14 | 15 | let mut escaped = false; 16 | let mut item = String::new(); 17 | 18 | for ch in source.chars() { 19 | if ch == ',' && !escaped { 20 | item = item.replace("\\,", ","); 21 | 22 | let mut new_item = String::new(); 23 | mem::swap(&mut item, &mut new_item); 24 | 25 | items.push(new_item); 26 | } else { 27 | item.push(ch); 28 | } 29 | escaped = ch == '\\'; 30 | } 31 | 32 | if !item.is_empty() { 33 | items.push(item.replace("\\,", ",")); 34 | } 35 | 36 | items 37 | } 38 | 39 | /// Escape special ASCII characters with a backslash. 40 | pub fn escape_filename<'t>(filename: &'t str) -> Cow<'t, str> { 41 | lazy_static! { 42 | static ref SPECIAL_CHARS: Regex = if cfg!(target_os = "windows") { 43 | // On Windows, don't escape `:` and `\`, as these are valid components of the path. 44 | Regex::new(r"[[:ascii:]&&[^0-9a-zA-Z._:\\-]]").unwrap() 45 | } else { 46 | // Similarly, don't escape `/` on other platforms. 47 | Regex::new(r"[[:ascii:]&&[^0-9a-zA-Z._/-]]").unwrap() 48 | }; 49 | } 50 | SPECIAL_CHARS.replace_all(&*filename, r"\$0") 51 | } 52 | 53 | /// Decode a file URI. 54 | /// 55 | /// - On UNIX: `file:///path/to/a%20file.ext` -> `/path/to/a file.ext` 56 | /// - On Windows: `file:///C:/path/to/a%20file.ext` -> `C:\path\to\a file.ext` 57 | pub fn decode_uri(uri: &str) -> Option { 58 | let path = match uri.split_at(8) { 59 | ("file:///", path) => path, 60 | _ => return None, 61 | }; 62 | let path = percent_decode(path.as_bytes()).decode_utf8().ok()?; 63 | if cfg!(target_os = "windows") { 64 | lazy_static! { 65 | static ref SLASH: Regex = Regex::new(r"/").unwrap(); 66 | } 67 | Some(String::from(SLASH.replace_all(&*path, r"\"))) 68 | } else { 69 | Some("/".to_owned() + &path) 70 | } 71 | } 72 | 73 | /// info text 74 | pub fn about_comments() -> String { 75 | format!( 76 | "Build on top of neovim\n\ 77 | Minimum supported neovim version: {}", 78 | shell::MINIMUM_SUPPORTED_NVIM_VERSION 79 | ) 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use super::*; 85 | 86 | #[test] 87 | fn test_comma_split() { 88 | let res = split_at_comma("a,b"); 89 | assert_eq!(2, res.len()); 90 | assert_eq!("a", res[0]); 91 | assert_eq!("b", res[1]); 92 | 93 | let res = split_at_comma("a,b\\,c"); 94 | assert_eq!(2, res.len()); 95 | assert_eq!("a", res[0]); 96 | assert_eq!("b,c", res[1]); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/mode.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use neovim_lib::Value; 3 | 4 | #[derive(Clone, PartialEq)] 5 | pub enum NvimMode { 6 | Normal, 7 | Insert, 8 | Other, 9 | } 10 | 11 | pub struct Mode { 12 | mode: NvimMode, 13 | idx: usize, 14 | info: Option>, 15 | } 16 | 17 | impl Mode { 18 | pub fn new() -> Self { 19 | Mode { 20 | mode: NvimMode::Normal, 21 | idx: 0, 22 | info: None, 23 | } 24 | } 25 | 26 | pub fn is(&self, mode: &NvimMode) -> bool { 27 | self.mode == *mode 28 | } 29 | 30 | pub fn mode_info(&self) -> Option<&ModeInfo> { 31 | self.info.as_ref().and_then(|i| i.get(self.idx)) 32 | } 33 | 34 | pub fn update(&mut self, mode: &str, idx: usize) { 35 | match mode { 36 | "normal" => self.mode = NvimMode::Normal, 37 | "insert" => self.mode = NvimMode::Insert, 38 | _ => self.mode = NvimMode::Other, 39 | } 40 | 41 | self.idx = idx; 42 | } 43 | 44 | pub fn set_info(&mut self, cursor_style_enabled: bool, info: Vec) { 45 | self.info = if cursor_style_enabled { 46 | Some(info) 47 | } else { 48 | None 49 | }; 50 | } 51 | } 52 | 53 | 54 | #[derive(Debug, PartialEq, Clone)] 55 | pub enum CursorShape { 56 | Block, 57 | Horizontal, 58 | Vertical, 59 | Unknown, 60 | } 61 | 62 | impl CursorShape { 63 | fn new(shape_code: &Value) -> Result { 64 | let str_code = shape_code 65 | .as_str() 66 | .ok_or_else(|| "Can't convert cursor shape to string".to_owned())?; 67 | 68 | Ok(match str_code { 69 | "block" => CursorShape::Block, 70 | "horizontal" => CursorShape::Horizontal, 71 | "vertical" => CursorShape::Vertical, 72 | _ => { 73 | error!("Unknown cursor_shape {}", str_code); 74 | CursorShape::Unknown 75 | } 76 | }) 77 | } 78 | } 79 | 80 | #[derive(Debug, PartialEq, Clone)] 81 | pub struct ModeInfo { 82 | cursor_shape: Option, 83 | cell_percentage: Option, 84 | pub blinkwait: Option, 85 | } 86 | 87 | impl ModeInfo { 88 | pub fn new(mode_info_map: &HashMap) -> Result { 89 | let cursor_shape = if let Some(shape) = mode_info_map.get("cursor_shape") { 90 | Some(CursorShape::new(shape)?) 91 | } else { 92 | None 93 | }; 94 | 95 | Ok(ModeInfo { 96 | cursor_shape, 97 | cell_percentage: mode_info_map.get("cell_percentage").and_then(|cp| cp.as_u64()), 98 | blinkwait: mode_info_map.get("blinkwait").and_then(|cp| cp.as_u64()).map(|v| v as u32), 99 | }) 100 | } 101 | 102 | pub fn cursor_shape(&self) -> Option<&CursorShape> { 103 | self.cursor_shape.as_ref() 104 | } 105 | 106 | pub fn cell_percentage(&self) -> u64 { 107 | self.cell_percentage.unwrap_or(0) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/nvim/client.rs: -------------------------------------------------------------------------------- 1 | use std::cell::{Cell, RefCell, RefMut}; 2 | use std::ops::{Deref, DerefMut}; 3 | use std::sync::{Arc, Mutex, MutexGuard}; 4 | 5 | use super::ErrorReport; 6 | use neovim_lib::{Neovim, NeovimApi}; 7 | 8 | #[derive(Clone, Copy, PartialEq)] 9 | enum NeovimClientState { 10 | Uninitialized, 11 | InitInProgress, 12 | Initialized, 13 | Error, 14 | } 15 | 16 | pub enum NeovimRef<'a> { 17 | SingleThreaded(RefMut<'a, Neovim>), 18 | MultiThreaded(MutexGuard<'a, Option>), 19 | } 20 | 21 | impl<'a> NeovimRef<'a> { 22 | fn from_nvim(nvim: RefMut<'a, Neovim>) -> Self { 23 | NeovimRef::SingleThreaded(nvim) 24 | } 25 | 26 | fn try_nvim_async(nvim_async: &'a NeovimClientAsync) -> Option> { 27 | let guard = nvim_async.nvim.try_lock(); 28 | 29 | if let Ok(guard) = guard { 30 | if guard.is_some() { 31 | return Some(NeovimRef::MultiThreaded(guard)); 32 | } 33 | } 34 | 35 | None 36 | } 37 | 38 | fn from_nvim_async(nvim_async: &'a NeovimClientAsync) -> Option> { 39 | let guard = nvim_async.nvim.lock().unwrap(); 40 | 41 | if guard.is_some() { 42 | Some(NeovimRef::MultiThreaded(guard)) 43 | } else { 44 | None 45 | } 46 | } 47 | 48 | pub fn non_blocked(mut self) -> Option { 49 | self.get_mode().ok_and_report().and_then(|mode| { 50 | mode.iter() 51 | .find(|kv| kv.0.as_str().map(|key| key == "blocking").unwrap_or(false)) 52 | .map(|kv| kv.1.as_bool().unwrap_or(false)) 53 | .and_then(|block| if block { None } else { Some(self) }) 54 | }) 55 | } 56 | } 57 | 58 | impl<'a> Deref for NeovimRef<'a> { 59 | type Target = Neovim; 60 | 61 | fn deref(&self) -> &Neovim { 62 | match *self { 63 | NeovimRef::SingleThreaded(ref nvim) => &*nvim, 64 | NeovimRef::MultiThreaded(ref nvim) => (&*nvim).as_ref().unwrap(), 65 | } 66 | } 67 | } 68 | 69 | impl<'a> DerefMut for NeovimRef<'a> { 70 | fn deref_mut(&mut self) -> &mut Neovim { 71 | match *self { 72 | NeovimRef::SingleThreaded(ref mut nvim) => &mut *nvim, 73 | NeovimRef::MultiThreaded(ref mut nvim) => (&mut *nvim).as_mut().unwrap(), 74 | } 75 | } 76 | } 77 | 78 | pub struct NeovimClientAsync { 79 | nvim: Arc>>, 80 | } 81 | 82 | impl NeovimClientAsync { 83 | fn new() -> Self { 84 | NeovimClientAsync { 85 | nvim: Arc::new(Mutex::new(None)), 86 | } 87 | } 88 | 89 | pub fn borrow(&self) -> Option { 90 | NeovimRef::from_nvim_async(self) 91 | } 92 | 93 | pub fn try_borrow(&self) -> Option { 94 | NeovimRef::try_nvim_async(self) 95 | } 96 | } 97 | 98 | impl Clone for NeovimClientAsync { 99 | fn clone(&self) -> Self { 100 | NeovimClientAsync { 101 | nvim: self.nvim.clone(), 102 | } 103 | } 104 | } 105 | 106 | pub struct NeovimClient { 107 | state: Cell, 108 | nvim: RefCell>, 109 | nvim_async: NeovimClientAsync, 110 | } 111 | 112 | impl NeovimClient { 113 | pub fn new() -> Self { 114 | NeovimClient { 115 | state: Cell::new(NeovimClientState::Uninitialized), 116 | nvim: RefCell::new(None), 117 | nvim_async: NeovimClientAsync::new(), 118 | } 119 | } 120 | 121 | pub fn clear(&self) { 122 | let mut nvim = self.nvim.borrow_mut(); 123 | if nvim.is_some() { 124 | nvim.take(); 125 | } else { 126 | self.nvim_async.nvim.lock().unwrap().take(); 127 | } 128 | } 129 | 130 | pub fn async_to_sync(&self) { 131 | let mut lock = self.nvim_async.nvim.lock().unwrap(); 132 | let nvim = lock.take().unwrap(); 133 | *self.nvim.borrow_mut() = Some(nvim); 134 | } 135 | 136 | pub fn set_nvim_async(&self, nvim: Neovim) -> NeovimClientAsync { 137 | *self.nvim_async.nvim.lock().unwrap() = Some(nvim); 138 | self.nvim_async.clone() 139 | } 140 | 141 | pub fn set_initialized(&self) { 142 | self.state.set(NeovimClientState::Initialized); 143 | } 144 | 145 | pub fn set_error(&self) { 146 | self.state.set(NeovimClientState::Error); 147 | } 148 | 149 | pub fn set_in_progress(&self) { 150 | self.state.set(NeovimClientState::InitInProgress); 151 | } 152 | 153 | pub fn is_initialized(&self) -> bool { 154 | self.state.get() == NeovimClientState::Initialized 155 | } 156 | 157 | pub fn is_uninitialized(&self) -> bool { 158 | self.state.get() == NeovimClientState::Uninitialized 159 | } 160 | 161 | pub fn is_initializing(&self) -> bool { 162 | self.state.get() == NeovimClientState::InitInProgress 163 | } 164 | 165 | /// In case neovimref locked in another thread 166 | /// this method can return None 167 | pub fn try_nvim(&self) -> Option { 168 | let nvim = self.nvim.borrow_mut(); 169 | if nvim.is_some() { 170 | Some(NeovimRef::from_nvim(RefMut::map(nvim, |n| { 171 | n.as_mut().unwrap() 172 | }))) 173 | } else { 174 | self.nvim_async.try_borrow() 175 | } 176 | } 177 | 178 | pub fn nvim(&self) -> Option { 179 | let nvim = self.nvim.borrow_mut(); 180 | if nvim.is_some() { 181 | Some(NeovimRef::from_nvim(RefMut::map(nvim, |n| { 182 | n.as_mut().unwrap() 183 | }))) 184 | } else { 185 | self.nvim_async.borrow() 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/nvim/ext.rs: -------------------------------------------------------------------------------- 1 | use std::result; 2 | 3 | use neovim_lib::CallError; 4 | 5 | pub trait ErrorReport { 6 | fn report_err(&self); 7 | 8 | fn ok_and_report(self) -> Option; 9 | } 10 | 11 | impl ErrorReport for result::Result { 12 | fn report_err(&self) { 13 | if let Err(ref err) = *self { 14 | error!("{}", err); 15 | } 16 | } 17 | 18 | fn ok_and_report(self) -> Option { 19 | self.report_err(); 20 | self.ok() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/nvim/handler.rs: -------------------------------------------------------------------------------- 1 | use std::result; 2 | use std::sync::{mpsc, Arc}; 3 | 4 | use neovim_lib::{Handler, RequestHandler, Value}; 5 | 6 | use crate::ui::UiMutex; 7 | use crate::shell; 8 | use glib; 9 | 10 | use super::repaint_mode::RepaintMode; 11 | use super::redraw_handler; 12 | 13 | pub struct NvimHandler { 14 | shell: Arc>, 15 | 16 | delayed_redraw_event_id: Arc>>, 17 | } 18 | 19 | impl NvimHandler { 20 | pub fn new(shell: Arc>) -> NvimHandler { 21 | NvimHandler { 22 | shell, 23 | delayed_redraw_event_id: Arc::new(UiMutex::new(None)), 24 | } 25 | } 26 | 27 | pub fn schedule_redraw_event(&self, event: Value) { 28 | let shell = self.shell.clone(); 29 | let delayed_redraw_event_id = self.delayed_redraw_event_id.clone(); 30 | 31 | glib::idle_add(move || { 32 | let id = Some(glib::timeout_add( 33 | 250, 34 | clone!(shell, event, delayed_redraw_event_id => move || { 35 | delayed_redraw_event_id.replace(None); 36 | 37 | if let Err(msg) = call_redraw_handler(vec![event.clone()], &shell) { 38 | error!("Error call function: {}", msg); 39 | } 40 | 41 | glib::Continue(false) 42 | }), 43 | )); 44 | 45 | delayed_redraw_event_id.replace(id); 46 | 47 | glib::Continue(false) 48 | }); 49 | } 50 | 51 | pub fn remove_scheduled_redraw_event(&self) { 52 | let delayed_redraw_event_id = self.delayed_redraw_event_id.clone(); 53 | glib::idle_add(move || { 54 | let id = delayed_redraw_event_id.replace(None); 55 | if let Some(ev_id) = id { 56 | glib::source_remove(ev_id); 57 | } 58 | 59 | glib::Continue(false) 60 | }); 61 | } 62 | 63 | fn nvim_cb(&self, method: &str, mut params: Vec) { 64 | match method { 65 | "redraw" => { 66 | redraw_handler::remove_or_delay_uneeded_events(self, &mut params); 67 | 68 | self.safe_call(move |ui| call_redraw_handler(params, ui)); 69 | } 70 | "Gui" => { 71 | if !params.is_empty() { 72 | let mut params_iter = params.into_iter(); 73 | if let Some(ev_name) = params_iter.next() { 74 | if let Value::String(ev_name) = ev_name { 75 | let args = params_iter.collect(); 76 | self.safe_call(move |ui| { 77 | let ui = &mut ui.borrow_mut(); 78 | redraw_handler::call_gui_event( 79 | ui, 80 | ev_name 81 | .as_str() 82 | .ok_or_else(|| "Event name does not exists")?, 83 | args, 84 | )?; 85 | ui.on_redraw(&RepaintMode::All); 86 | Ok(()) 87 | }); 88 | } else { 89 | error!("Unsupported event"); 90 | } 91 | } else { 92 | error!("Event name does not exists"); 93 | } 94 | } else { 95 | error!("Unsupported event {:?}", params); 96 | } 97 | } 98 | "subscription" => { 99 | self.safe_call(move |ui| { 100 | let ui = &ui.borrow(); 101 | ui.notify(params) 102 | }); 103 | } 104 | _ => { 105 | error!("Notification {}({:?})", method, params); 106 | } 107 | } 108 | } 109 | 110 | fn nvim_cb_req(&self, method: &str, params: Vec) -> result::Result { 111 | match method { 112 | "Gui" => { 113 | if !params.is_empty() { 114 | let mut params_iter = params.into_iter(); 115 | if let Some(req_name) = params_iter.next() { 116 | if let Value::String(req_name) = req_name { 117 | let args = params_iter.collect(); 118 | let (sender, receiver) = mpsc::channel(); 119 | self.safe_call(move |ui| { 120 | sender 121 | .send(redraw_handler::call_gui_request( 122 | &ui.clone(), 123 | req_name 124 | .as_str() 125 | .ok_or_else(|| "Event name does not exists")?, 126 | &args, 127 | )) 128 | .unwrap(); 129 | { 130 | let ui = &mut ui.borrow_mut(); 131 | ui.on_redraw(&RepaintMode::All); 132 | } 133 | Ok(()) 134 | }); 135 | Ok(receiver.recv().unwrap()?) 136 | } else { 137 | error!("Unsupported request"); 138 | Err(Value::Nil) 139 | } 140 | } else { 141 | error!("Request name does not exist"); 142 | Err(Value::Nil) 143 | } 144 | } else { 145 | error!("Unsupported request {:?}", params); 146 | Err(Value::Nil) 147 | } 148 | } 149 | _ => { 150 | error!("Request {}({:?})", method, params); 151 | Err(Value::Nil) 152 | } 153 | } 154 | } 155 | 156 | fn safe_call(&self, cb: F) 157 | where 158 | F: FnOnce(&Arc>) -> result::Result<(), String> + 'static + Send, 159 | { 160 | safe_call(self.shell.clone(), cb); 161 | } 162 | } 163 | 164 | fn call_redraw_handler( 165 | params: Vec, 166 | ui: &Arc>, 167 | ) -> result::Result<(), String> { 168 | let ui = &mut ui.borrow_mut(); 169 | let mut repaint_mode = RepaintMode::Nothing; 170 | 171 | for ev in params { 172 | if let Value::Array(ev_args) = ev { 173 | let mut args_iter = ev_args.into_iter(); 174 | let ev_name = args_iter.next(); 175 | if let Some(ev_name) = ev_name { 176 | if let Some(ev_name) = ev_name.as_str() { 177 | for local_args in args_iter { 178 | let args = match local_args { 179 | Value::Array(ar) => ar, 180 | _ => vec![], 181 | }; 182 | let call_reapint_mode = match redraw_handler::call(ui, &ev_name, args) { 183 | Ok(mode) => mode, 184 | Err(desc) => return Err(format!("Event {}\n{}", ev_name, desc)), 185 | }; 186 | repaint_mode = repaint_mode.join(call_reapint_mode); 187 | } 188 | } else { 189 | error!("Unsupported event"); 190 | } 191 | } else { 192 | error!("Event name does not exists"); 193 | } 194 | } else { 195 | error!("Unsupported event type {:?}", ev); 196 | } 197 | } 198 | 199 | ui.on_redraw(&repaint_mode); 200 | Ok(()) 201 | } 202 | 203 | fn safe_call(shell: Arc>, cb: F) 204 | where 205 | F: FnOnce(&Arc>) -> result::Result<(), String> + 'static + Send, 206 | { 207 | let mut cb = Some(cb); 208 | glib::idle_add(move || { 209 | if let Err(msg) = cb.take().unwrap()(&shell) { 210 | error!("Error call function: {}", msg); 211 | } 212 | glib::Continue(false) 213 | }); 214 | } 215 | 216 | impl Handler for NvimHandler { 217 | fn handle_notify(&mut self, name: &str, args: Vec) { 218 | self.nvim_cb(name, args); 219 | } 220 | 221 | } 222 | 223 | impl RequestHandler for NvimHandler { 224 | fn handle_request(&mut self, name: &str, args: Vec) -> result::Result { 225 | self.nvim_cb_req(name, args) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/nvim/mod.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod handler; 3 | mod redraw_handler; 4 | mod repaint_mode; 5 | mod ext; 6 | 7 | pub use self::redraw_handler::{CompleteItem, NvimCommand}; 8 | pub use self::repaint_mode::RepaintMode; 9 | pub use self::client::{NeovimClient, NeovimClientAsync, NeovimRef}; 10 | pub use self::ext::ErrorReport; 11 | pub use self::handler::NvimHandler; 12 | 13 | use std::error; 14 | use std::fmt; 15 | use std::env; 16 | use std::process::{Command, Stdio}; 17 | use std::result; 18 | use std::time::Duration; 19 | 20 | use neovim_lib::{Neovim, NeovimApi, Session, UiAttachOptions}; 21 | 22 | use crate::nvim_config::NvimConfig; 23 | 24 | #[derive(Debug)] 25 | pub struct NvimInitError { 26 | source: Box, 27 | cmd: Option, 28 | } 29 | 30 | impl NvimInitError { 31 | pub fn new_post_init(error: E) -> NvimInitError 32 | where 33 | E: Into>, 34 | { 35 | NvimInitError { 36 | cmd: None, 37 | source: error.into(), 38 | } 39 | } 40 | 41 | pub fn new(cmd: &Command, error: E) -> NvimInitError 42 | where 43 | E: Into>, 44 | { 45 | NvimInitError { 46 | cmd: Some(format!("{:?}", cmd)), 47 | source: error.into(), 48 | } 49 | } 50 | 51 | pub fn source(&self) -> String { 52 | format!("{}", self.source) 53 | } 54 | 55 | pub fn cmd(&self) -> Option<&String> { 56 | self.cmd.as_ref() 57 | } 58 | } 59 | 60 | impl fmt::Display for NvimInitError { 61 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 62 | write!(f, "{:?}", self.source) 63 | } 64 | } 65 | 66 | impl error::Error for NvimInitError { 67 | fn description(&self) -> &str { 68 | "Can't start nvim instance" 69 | } 70 | 71 | fn cause(&self) -> Option<&dyn error::Error> { 72 | Some(&*self.source) 73 | } 74 | } 75 | 76 | #[cfg(target_os = "windows")] 77 | fn set_windows_creation_flags(cmd: &mut Command) { 78 | use std::os::windows::process::CommandExt; 79 | cmd.creation_flags(0x08000000); // CREATE_NO_WINDOW 80 | } 81 | 82 | pub fn start( 83 | handler: NvimHandler, 84 | nvim_bin_path: Option<&String>, 85 | timeout: Option, 86 | args_for_neovim: Vec, 87 | ) -> result::Result { 88 | let mut cmd = if let Some(path) = nvim_bin_path { 89 | Command::new(path) 90 | } else { 91 | Command::new("nvim") 92 | }; 93 | 94 | cmd.arg("--embed") 95 | .arg("--cmd") 96 | .arg("set termguicolors") 97 | .arg("--cmd") 98 | .arg("let g:GtkGuiLoaded = 1") 99 | .stderr(Stdio::inherit()); 100 | 101 | #[cfg(target_os = "windows")] 102 | set_windows_creation_flags(&mut cmd); 103 | 104 | if let Ok(runtime_path) = env::var("NVIM_GTK_RUNTIME_PATH") { 105 | cmd.arg("--cmd") 106 | .arg(format!("let &rtp.=',{}'", runtime_path)); 107 | } else if let Some(prefix) = option_env!("PREFIX") { 108 | cmd.arg("--cmd") 109 | .arg(format!("let &rtp.=',{}/share/nvim-gtk/runtime'", prefix)); 110 | } else { 111 | cmd.arg("--cmd").arg("let &rtp.=',runtime'"); 112 | } 113 | 114 | if let Some(nvim_config) = NvimConfig::config_path() { 115 | if let Some(path) = nvim_config.to_str() { 116 | cmd.arg("--cmd").arg(format!("source {}", path)); 117 | } 118 | } 119 | 120 | for arg in args_for_neovim { 121 | cmd.arg(arg); 122 | } 123 | 124 | let session = Session::new_child_cmd(&mut cmd); 125 | 126 | let mut session = match session { 127 | Err(e) => return Err(NvimInitError::new(&cmd, e)), 128 | Ok(s) => s, 129 | }; 130 | 131 | session.set_timeout(timeout.unwrap_or(Duration::from_millis(10_000))); 132 | 133 | let mut nvim = Neovim::new(session); 134 | 135 | nvim.session.start_event_loop_handler(handler); 136 | 137 | Ok(nvim) 138 | } 139 | 140 | pub fn post_start_init( 141 | nvim: NeovimClientAsync, 142 | cols: i64, 143 | rows: i64, 144 | input_data: Option, 145 | ) -> result::Result<(), NvimInitError> { 146 | nvim.borrow() 147 | .unwrap() 148 | .ui_attach( 149 | cols, 150 | rows, 151 | UiAttachOptions::new() 152 | .set_popupmenu_external(true) 153 | .set_tabline_external(true) 154 | .set_linegrid_external(true) 155 | .set_hlstate_external(true) 156 | ) 157 | .map_err(NvimInitError::new_post_init)?; 158 | 159 | nvim.borrow() 160 | .unwrap() 161 | .command("runtime! ginit.vim") 162 | .map_err(NvimInitError::new_post_init)?; 163 | 164 | if let Some(input_data) = input_data { 165 | let mut nvim = nvim.borrow().unwrap(); 166 | let buf = nvim.get_current_buf().ok_and_report(); 167 | 168 | if let Some(buf) = buf { 169 | buf.set_lines( 170 | &mut *nvim, 171 | 0, 172 | 0, 173 | true, 174 | input_data.lines().map(|l| l.to_owned()).collect(), 175 | ).report_err(); 176 | } 177 | } 178 | 179 | Ok(()) 180 | } 181 | -------------------------------------------------------------------------------- /src/nvim/repaint_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::ui_model::{ModelRect, ModelRectVec}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub enum RepaintMode { 5 | Nothing, 6 | All, 7 | AreaList(ModelRectVec), 8 | Area(ModelRect), 9 | } 10 | 11 | impl RepaintMode { 12 | pub fn join(self, mode: RepaintMode) -> RepaintMode { 13 | match (self, mode) { 14 | (RepaintMode::Nothing, m) => m, 15 | (m, RepaintMode::Nothing) => m, 16 | (RepaintMode::All, _) => RepaintMode::All, 17 | (_, RepaintMode::All) => RepaintMode::All, 18 | (RepaintMode::Area(mr1), RepaintMode::Area(mr2)) => { 19 | let mut vec = ModelRectVec::new(mr1); 20 | vec.join(&mr2); 21 | RepaintMode::AreaList(vec) 22 | } 23 | (RepaintMode::AreaList(mut target), RepaintMode::AreaList(source)) => { 24 | for s in &source.list { 25 | target.join(s); 26 | } 27 | RepaintMode::AreaList(target) 28 | } 29 | (RepaintMode::AreaList(mut list), RepaintMode::Area(l2)) => { 30 | list.join(&l2); 31 | RepaintMode::AreaList(list) 32 | } 33 | (RepaintMode::Area(l1), RepaintMode::AreaList(mut list)) => { 34 | list.join(&l1); 35 | RepaintMode::AreaList(list) 36 | } 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn test_mode() { 47 | let mode = RepaintMode::Area(ModelRect::point(1, 1)); 48 | let mode = mode.join(RepaintMode::Nothing); 49 | 50 | match mode { 51 | RepaintMode::Area(ref rect) => { 52 | assert_eq!(1, rect.top); 53 | assert_eq!(1, rect.bot); 54 | assert_eq!(1, rect.left); 55 | assert_eq!(1, rect.right); 56 | } 57 | _ => panic!("mode is worng"), 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/nvim_config.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | use std::fs::{remove_file, OpenOptions}; 3 | use std::io::Write; 4 | 5 | use crate::dirs; 6 | use crate::plug_manager; 7 | 8 | #[derive(Clone)] 9 | pub struct NvimConfig { 10 | plug_config: Option, 11 | } 12 | 13 | impl NvimConfig { 14 | const CONFIG_PATH: &'static str = "settings.vim"; 15 | 16 | pub fn new(plug_config: Option) -> Self { 17 | NvimConfig { plug_config } 18 | } 19 | 20 | pub fn generate_config(&self) -> Option { 21 | if self.plug_config.is_some() { 22 | match self.write_file() { 23 | Err(err) => { 24 | error!("{}", err); 25 | None 26 | } 27 | Ok(file) => Some(file), 28 | } 29 | } else { 30 | NvimConfig::config_path().map(remove_file); 31 | None 32 | } 33 | } 34 | 35 | pub fn config_path() -> Option { 36 | if let Ok(mut path) = dirs::get_app_config_dir() { 37 | path.push(NvimConfig::CONFIG_PATH); 38 | if path.is_file() { 39 | return Some(path); 40 | } 41 | } 42 | 43 | None 44 | } 45 | 46 | fn write_file(&self) -> Result { 47 | let mut config_dir = dirs::get_app_config_dir_create()?; 48 | config_dir.push(NvimConfig::CONFIG_PATH); 49 | 50 | let mut file = OpenOptions::new() 51 | .create(true) 52 | .write(true) 53 | .truncate(true) 54 | .open(&config_dir) 55 | .map_err(|e| format!("{}", e))?; 56 | 57 | let content = &self.plug_config.as_ref().unwrap().source; 58 | if !content.is_empty() { 59 | debug!("{}", content); 60 | file.write_all(content.as_bytes()).map_err( 61 | |e| format!("{}", e), 62 | )?; 63 | } 64 | 65 | file.sync_all().map_err(|e| format!("{}", e))?; 66 | Ok(config_dir) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/plug_manager/manager.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use super::vim_plug; 4 | use super::store::{Store, PlugInfo}; 5 | 6 | use crate::nvim::NeovimClient; 7 | 8 | pub struct Manager { 9 | pub vim_plug: vim_plug::Manager, 10 | pub store: Store, 11 | pub plug_manage_state: PlugManageState, 12 | } 13 | 14 | impl Manager { 15 | pub fn new() -> Self { 16 | let (plug_manage_state, store) = if Store::is_config_exists() { 17 | (PlugManageState::NvimGtk, Store::load()) 18 | } else { 19 | (PlugManageState::Unknown, Default::default()) 20 | }; 21 | 22 | Manager { 23 | vim_plug: vim_plug::Manager::new(), 24 | plug_manage_state, 25 | store, 26 | } 27 | } 28 | 29 | pub fn generate_config(&self) -> Option { 30 | if self.store.is_enabled() { 31 | Some(PlugManagerConfigSource::new(&self.store)) 32 | } else { 33 | None 34 | } 35 | } 36 | 37 | pub fn init_nvim_client(&mut self, nvim: Rc) { 38 | self.vim_plug.initialize(nvim); 39 | } 40 | 41 | pub fn reload_store(&mut self) { 42 | match self.plug_manage_state { 43 | PlugManageState::Unknown => { 44 | if self.vim_plug.is_loaded() { 45 | self.store = Store::load_from_plug(&self.vim_plug); 46 | self.plug_manage_state = PlugManageState::VimPlug; 47 | } else { 48 | self.store = Default::default(); 49 | } 50 | } 51 | PlugManageState::NvimGtk => { 52 | if Store::is_config_exists() { 53 | self.store = Store::load(); 54 | } else { 55 | self.store = Default::default(); 56 | } 57 | } 58 | PlugManageState::VimPlug => { 59 | if Store::is_config_exists() { 60 | self.store = Store::load(); 61 | self.plug_manage_state = PlugManageState::NvimGtk; 62 | } else { 63 | self.store = Default::default(); 64 | } 65 | } 66 | } 67 | if let PlugManageState::Unknown = self.plug_manage_state { 68 | if self.vim_plug.is_loaded() { 69 | self.store = Store::load_from_plug(&self.vim_plug); 70 | self.plug_manage_state = PlugManageState::VimPlug; 71 | } 72 | } 73 | } 74 | 75 | pub fn save(&self) { 76 | self.store.save(); 77 | } 78 | 79 | pub fn clear_removed(&mut self) { 80 | self.store.clear_removed(); 81 | } 82 | 83 | pub fn add_plug(&mut self, plug: PlugInfo) -> bool { 84 | self.store.add_plug(plug) 85 | } 86 | 87 | pub fn move_item(&mut self, idx: usize, offset: i32) { 88 | self.store.move_item(idx, offset); 89 | } 90 | } 91 | 92 | pub enum PlugManageState { 93 | NvimGtk, 94 | VimPlug, 95 | Unknown, 96 | } 97 | 98 | #[derive(Clone)] 99 | pub struct PlugManagerConfigSource { 100 | pub source: String, 101 | } 102 | 103 | impl PlugManagerConfigSource { 104 | pub fn new(store: &Store) -> Self { 105 | let mut builder = "call plug#begin()\n".to_owned(); 106 | 107 | for plug in store.get_plugs() { 108 | if !plug.removed { 109 | builder += &format!( 110 | "Plug '{}', {{ 'as': '{}' }}\n", 111 | plug.get_plug_path(), 112 | plug.name 113 | ); 114 | } 115 | } 116 | 117 | builder += "call plug#end()\n"; 118 | 119 | PlugManagerConfigSource { source: builder } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/plug_manager/mod.rs: -------------------------------------------------------------------------------- 1 | mod ui; 2 | mod vim_plug; 3 | mod store; 4 | mod manager; 5 | mod plugin_settings_dlg; 6 | mod vimawesome; 7 | 8 | pub use self::ui::Ui; 9 | pub use self::manager::{Manager, PlugManagerConfigSource}; 10 | -------------------------------------------------------------------------------- /src/plug_manager/plugin_settings_dlg.rs: -------------------------------------------------------------------------------- 1 | use gtk; 2 | use gtk::prelude::*; 3 | 4 | use super::store; 5 | 6 | pub struct Builder<'a> { 7 | title: &'a str, 8 | } 9 | 10 | impl<'a> Builder<'a> { 11 | pub fn new(title: &'a str) -> Self { 12 | Builder { title } 13 | } 14 | 15 | pub fn show>(&self, parent: &F) -> Option { 16 | let dlg = gtk::Dialog::new_with_buttons( 17 | Some(self.title), 18 | Some(parent), 19 | gtk::DialogFlags::USE_HEADER_BAR | gtk::DialogFlags::DESTROY_WITH_PARENT, 20 | &[ 21 | ("Cancel", gtk::ResponseType::Cancel), 22 | ("Ok", gtk::ResponseType::Ok), 23 | ], 24 | ); 25 | 26 | let content = dlg.get_content_area(); 27 | let border = gtk::Box::new(gtk::Orientation::Horizontal, 0); 28 | border.set_border_width(12); 29 | 30 | let list = gtk::ListBox::new(); 31 | list.set_selection_mode(gtk::SelectionMode::None); 32 | 33 | let path = gtk::Box::new(gtk::Orientation::Horizontal, 5); 34 | path.set_border_width(5); 35 | let path_lbl = gtk::Label::new(Some("Repo")); 36 | let path_e = gtk::Entry::new(); 37 | path_e.set_placeholder_text(Some("user_name/repo_name")); 38 | 39 | path.pack_start(&path_lbl, true, true, 0); 40 | path.pack_end(&path_e, false, true, 0); 41 | 42 | list.add(&path); 43 | 44 | let name = gtk::Box::new(gtk::Orientation::Horizontal, 5); 45 | name.set_border_width(5); 46 | let name_lbl = gtk::Label::new(Some("Name")); 47 | let name_e = gtk::Entry::new(); 48 | 49 | name.pack_start(&name_lbl, true, true, 0); 50 | name.pack_end(&name_e, false, true, 0); 51 | 52 | list.add(&name); 53 | 54 | border.pack_start(&list, true, true, 0); 55 | content.add(&border); 56 | content.show_all(); 57 | 58 | path_e.connect_changed(clone!(name_e => move |p| { 59 | if let Some(name) = p.get_text().and_then(|t| extract_name(&t)) { 60 | name_e.set_text(&name); 61 | } 62 | })); 63 | 64 | let res = if dlg.run() == gtk::ResponseType::Ok { 65 | path_e.get_text().map(|path| { 66 | let name = name_e 67 | .get_text() 68 | .map(String::from) 69 | .and_then(|name| { 70 | if name.trim().is_empty() { 71 | None 72 | } else { 73 | Some(name.to_owned()) 74 | } 75 | }) 76 | .or_else(|| extract_name(path.as_str())) 77 | .unwrap_or_else(|| path.as_str().to_owned()); 78 | 79 | store::PlugInfo::new(name, path.to_owned()) 80 | }) 81 | } else { 82 | None 83 | }; 84 | 85 | dlg.destroy(); 86 | 87 | res 88 | } 89 | } 90 | 91 | fn extract_name(path: &str) -> Option { 92 | if let Some(idx) = path.rfind(|c| c == '/' || c == '\\') { 93 | if idx < path.len() - 1 { 94 | let path = path.trim_end_matches(".git"); 95 | Some(path[idx + 1..].to_owned()) 96 | } else { 97 | None 98 | } 99 | } else { 100 | None 101 | } 102 | } 103 | 104 | #[cfg(test)] 105 | mod tests { 106 | use super::*; 107 | 108 | #[test] 109 | fn test_extract_name() { 110 | assert_eq!( 111 | Some("plugin_name".to_owned()), 112 | extract_name("http://github.com/somebody/plugin_name.git") 113 | ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/plug_manager/store.rs: -------------------------------------------------------------------------------- 1 | use toml; 2 | 3 | use crate::settings::SettingsLoader; 4 | use super::vim_plug; 5 | 6 | #[derive(Default)] 7 | pub struct Store { 8 | settings: Settings, 9 | } 10 | 11 | impl Store { 12 | pub fn is_config_exists() -> bool { 13 | Settings::is_file_exists() 14 | } 15 | 16 | pub fn is_enabled(&self) -> bool { 17 | self.settings.enabled 18 | } 19 | 20 | pub fn load() -> Self { 21 | Store { settings: Settings::load() } 22 | } 23 | 24 | pub fn load_from_plug(vim_plug: &vim_plug::Manager) -> Self { 25 | let settings = match vim_plug.get_plugs() { 26 | Err(msg) => { 27 | error!("{}", msg); 28 | Default::default() 29 | } 30 | Ok(plugs) => { 31 | let plugs = plugs 32 | .iter() 33 | .map(|vpi| PlugInfo::new(vpi.name.to_owned(), vpi.uri.to_owned())) 34 | .collect(); 35 | Settings::new(plugs) 36 | } 37 | }; 38 | 39 | Store { settings } 40 | } 41 | 42 | pub fn get_plugs(&self) -> &[PlugInfo] { 43 | &self.settings.plugs 44 | } 45 | 46 | pub fn set_enabled(&mut self, enabled: bool) { 47 | self.settings.enabled = enabled; 48 | } 49 | 50 | pub fn clear_removed(&mut self) { 51 | self.settings.plugs.retain(|p| !p.removed); 52 | } 53 | 54 | pub fn save(&self) { 55 | self.settings.save(); 56 | } 57 | 58 | pub fn remove_plug(&mut self, idx: usize) { 59 | self.settings.plugs[idx].removed = true; 60 | } 61 | 62 | pub fn restore_plug(&mut self, idx: usize) { 63 | self.settings.plugs[idx].removed = false; 64 | } 65 | 66 | pub fn add_plug(&mut self, plug: PlugInfo) -> bool { 67 | let path = plug.get_plug_path(); 68 | if self.settings.plugs.iter().any(|p| { 69 | p.get_plug_path() == path || p.name == plug.name 70 | }) 71 | { 72 | return false; 73 | } 74 | self.settings.plugs.push(plug); 75 | true 76 | } 77 | 78 | pub fn plugs_count(&self) -> usize { 79 | self.settings.plugs.len() 80 | } 81 | 82 | pub fn move_item(&mut self, idx: usize, offset: i32) { 83 | let plug = self.settings.plugs.remove(idx); 84 | self.settings.plugs.insert( 85 | (idx as i32 + offset) as usize, 86 | plug, 87 | ); 88 | } 89 | } 90 | 91 | #[derive(Serialize, Deserialize)] 92 | struct Settings { 93 | enabled: bool, 94 | plugs: Vec, 95 | } 96 | 97 | impl Settings { 98 | fn new(plugs: Vec) -> Self { 99 | Settings { 100 | plugs, 101 | enabled: false, 102 | } 103 | } 104 | } 105 | 106 | impl Default for Settings { 107 | fn default() -> Self { 108 | Settings { 109 | plugs: vec![], 110 | enabled: false, 111 | } 112 | } 113 | } 114 | 115 | impl SettingsLoader for Settings { 116 | const SETTINGS_FILE: &'static str = "plugs.toml"; 117 | 118 | fn from_str(s: &str) -> Result { 119 | toml::from_str(&s).map_err(|e| format!("{}", e)) 120 | } 121 | } 122 | 123 | #[derive(Serialize, Deserialize)] 124 | pub struct PlugInfo { 125 | pub name: String, 126 | pub url: String, 127 | pub removed: bool, 128 | } 129 | 130 | impl PlugInfo { 131 | pub fn new(name: String, url: String) -> Self { 132 | PlugInfo { 133 | name, 134 | url, 135 | removed: false, 136 | } 137 | } 138 | 139 | pub fn get_plug_path(&self) -> String { 140 | if self.url.contains("github.com") { 141 | let mut path_comps: Vec<&str> = self.url 142 | .trim_end_matches(".git") 143 | .rsplit('/') 144 | .take(2) 145 | .collect(); 146 | path_comps.reverse(); 147 | path_comps.join("/") 148 | } else { 149 | self.url.clone() 150 | } 151 | } 152 | } 153 | 154 | #[cfg(test)] 155 | mod tests { 156 | use super::*; 157 | 158 | #[test] 159 | fn test_get_plug_path() { 160 | let plug = PlugInfo::new( 161 | "rust.vim".to_owned(), 162 | "https://git::@github.com/rust-lang/rust.vim.git".to_owned(), 163 | ); 164 | assert_eq!("rust-lang/rust.vim".to_owned(), plug.get_plug_path()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/plug_manager/vim_plug.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use neovim_lib::{NeovimApi, NeovimApiAsync}; 4 | 5 | use crate::nvim::{NeovimClient, ErrorReport, NeovimRef}; 6 | use crate::value::ValueMapExt; 7 | 8 | pub struct Manager { 9 | nvim: Option>, 10 | } 11 | 12 | impl Manager { 13 | pub fn new() -> Self { 14 | Manager { nvim: None } 15 | } 16 | 17 | pub fn initialize(&mut self, nvim: Rc) { 18 | self.nvim = Some(nvim); 19 | } 20 | 21 | fn nvim(&self) -> Option { 22 | self.nvim.as_ref().unwrap().nvim() 23 | } 24 | 25 | pub fn get_plugs(&self) -> Result, String> { 26 | if let Some(mut nvim) = self.nvim() { 27 | let g_plugs = nvim.eval("g:plugs").map_err(|e| { 28 | format!("Can't retrive g:plugs map: {}", e) 29 | })?; 30 | 31 | let plugs_map = g_plugs 32 | .as_map() 33 | .ok_or_else(|| "Can't retrive g:plugs map".to_owned())? 34 | .to_attrs_map()?; 35 | 36 | let g_plugs_order = nvim.eval("g:plugs_order").map_err(|e| format!("{}", e))?; 37 | 38 | let order_arr = g_plugs_order.as_array().ok_or_else( 39 | || "Can't find g:plugs_order array" 40 | .to_owned(), 41 | )?; 42 | 43 | let plugs_info: Vec = order_arr 44 | .iter() 45 | .map(|n| n.as_str()) 46 | .filter_map(|name| if let Some(name) = name { 47 | plugs_map 48 | .get(name) 49 | .and_then(|desc| desc.as_map()) 50 | .and_then(|desc| desc.to_attrs_map().ok()) 51 | .and_then(|desc| { 52 | let uri = desc.get("uri").and_then(|uri| uri.as_str()); 53 | if let Some(uri) = uri { 54 | Some(VimPlugInfo::new(name.to_owned(), uri.to_owned())) 55 | } else { 56 | None 57 | } 58 | }) 59 | } else { 60 | None 61 | }) 62 | .collect(); 63 | Ok(plugs_info.into_boxed_slice()) 64 | } else { 65 | Err("Nvim not initialized".to_owned()) 66 | } 67 | } 68 | 69 | pub fn is_loaded(&self) -> bool { 70 | if let Some(mut nvim) = self.nvim() { 71 | let loaded_plug = nvim.eval("exists('g:loaded_plug')"); 72 | loaded_plug 73 | .ok_and_report() 74 | .and_then(|loaded_plug| loaded_plug.as_i64()) 75 | .map_or(false, |loaded_plug| loaded_plug > 0) 76 | } else { 77 | false 78 | } 79 | } 80 | 81 | pub fn reload(&self, path: &str) { 82 | if let Some(mut nvim) = self.nvim() { 83 | nvim.command_async(&format!("source {}", path)) 84 | .cb(|r| r.report_err()) 85 | .call() 86 | } 87 | } 88 | } 89 | 90 | #[derive(Debug)] 91 | pub struct VimPlugInfo { 92 | pub name: String, 93 | pub uri: String, 94 | } 95 | 96 | impl VimPlugInfo { 97 | pub fn new(name: String, uri: String) -> Self { 98 | VimPlugInfo { name, uri } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/plug_manager/vimawesome.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::process::{Command, Stdio}; 3 | use std::rc::Rc; 4 | use std::thread; 5 | 6 | use serde_json; 7 | 8 | use glib; 9 | use gtk; 10 | use gtk::prelude::*; 11 | 12 | use super::store::PlugInfo; 13 | 14 | pub fn call(query: Option, cb: F) 15 | where 16 | F: FnOnce(io::Result) + Send + 'static, 17 | { 18 | thread::spawn(move || { 19 | let mut result = Some(request(query.as_ref().map(|s| s.as_ref()))); 20 | let mut cb = Some(cb); 21 | 22 | glib::idle_add(move || { 23 | cb.take().unwrap()(result.take().unwrap()); 24 | Continue(false) 25 | }) 26 | }); 27 | } 28 | 29 | fn request(query: Option<&str>) -> io::Result { 30 | let child = Command::new("curl") 31 | .arg("-s") 32 | .arg(format!( 33 | "https://vimawesome.com/api/plugins?query={}&page=1", 34 | query.unwrap_or("") 35 | )) 36 | .stdout(Stdio::piped()) 37 | .spawn()?; 38 | 39 | let out = child.wait_with_output()?; 40 | 41 | if out.status.success() { 42 | if out.stdout.is_empty() { 43 | Ok(DescriptionList::empty()) 44 | } else { 45 | let description_list: DescriptionList = serde_json::from_slice(&out.stdout) 46 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 47 | Ok(description_list) 48 | } 49 | } else { 50 | Err(io::Error::new( 51 | io::ErrorKind::Other, 52 | format!( 53 | "curl exit with error:\n{}", 54 | match out.status.code() { 55 | Some(code) => format!("Exited with status code: {}", code), 56 | None => "Process terminated by signal".to_owned(), 57 | } 58 | ), 59 | )) 60 | } 61 | } 62 | 63 | pub fn build_result_panel( 64 | list: &DescriptionList, 65 | add_cb: F, 66 | ) -> gtk::ScrolledWindow { 67 | let scroll = gtk::ScrolledWindow::new( 68 | Option::<>k::Adjustment>::None, 69 | Option::<>k::Adjustment>::None, 70 | ); 71 | scroll.get_style_context().add_class("view"); 72 | let panel = gtk::ListBox::new(); 73 | 74 | let cb_ref = Rc::new(add_cb); 75 | for plug in list.plugins.iter() { 76 | let row = create_plug_row(plug, cb_ref.clone()); 77 | 78 | panel.add(&row); 79 | } 80 | 81 | scroll.add(&panel); 82 | scroll.show_all(); 83 | scroll 84 | } 85 | 86 | fn create_plug_row( 87 | plug: &Description, 88 | add_cb: Rc, 89 | ) -> gtk::ListBoxRow { 90 | let row = gtk::ListBoxRow::new(); 91 | let row_container = gtk::Box::new(gtk::Orientation::Vertical, 5); 92 | row_container.set_border_width(5); 93 | let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 5); 94 | let label_box = create_plug_label(plug); 95 | 96 | let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); 97 | button_box.set_halign(gtk::Align::End); 98 | 99 | let add_btn = gtk::Button::new_with_label("Install"); 100 | button_box.pack_start(&add_btn, false, true, 0); 101 | 102 | row_container.pack_start(&hbox, true, true, 0); 103 | hbox.pack_start(&label_box, true, true, 0); 104 | hbox.pack_start(&button_box, false, true, 0); 105 | 106 | row.add(&row_container); 107 | 108 | add_btn.connect_clicked(clone!(plug => move |btn| { 109 | if let Some(ref github_url) = plug.github_url { 110 | btn.set_sensitive(false); 111 | add_cb(PlugInfo::new(plug.name.clone(), github_url.clone())); 112 | } 113 | })); 114 | 115 | row 116 | } 117 | 118 | fn create_plug_label(plug: &Description) -> gtk::Box { 119 | let label_box = gtk::Box::new(gtk::Orientation::Vertical, 5); 120 | 121 | let name_lbl = gtk::Label::new(None); 122 | name_lbl.set_markup(&format!( 123 | "{} by {}", 124 | plug.name, 125 | plug.author 126 | .as_ref() 127 | .map(|s| s.as_ref()) 128 | .unwrap_or("unknown",) 129 | )); 130 | name_lbl.set_halign(gtk::Align::Start); 131 | let url_lbl = gtk::Label::new(None); 132 | if let Some(url) = plug.github_url.as_ref() { 133 | url_lbl.set_markup(&format!("{}", url, url)); 134 | } 135 | url_lbl.set_halign(gtk::Align::Start); 136 | 137 | label_box.pack_start(&name_lbl, true, true, 0); 138 | label_box.pack_start(&url_lbl, true, true, 0); 139 | label_box 140 | } 141 | 142 | #[derive(Deserialize, Debug)] 143 | pub struct DescriptionList { 144 | pub plugins: Box<[Description]>, 145 | } 146 | 147 | impl DescriptionList { 148 | fn empty() -> DescriptionList { 149 | DescriptionList { 150 | plugins: Box::new([]), 151 | } 152 | } 153 | } 154 | 155 | #[derive(Deserialize, Debug, Clone)] 156 | pub struct Description { 157 | pub name: String, 158 | pub github_url: Option, 159 | pub author: Option, 160 | pub github_stars: Option, 161 | } 162 | -------------------------------------------------------------------------------- /src/render/context.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use pango; 4 | 5 | use crate::sys::pango as sys_pango; 6 | 7 | use super::itemize::ItemizeIterator; 8 | use crate::ui_model::StyledLine; 9 | 10 | pub struct Context { 11 | font_metrics: FontMetrix, 12 | font_features: FontFeatures, 13 | line_space: i32, 14 | } 15 | 16 | impl Context { 17 | pub fn new(pango_context: pango::Context) -> Self { 18 | Context { 19 | line_space: 0, 20 | font_metrics: FontMetrix::new(pango_context, 0), 21 | font_features: FontFeatures::new(), 22 | } 23 | } 24 | 25 | pub fn update(&mut self, pango_context: pango::Context) { 26 | self.font_metrics = FontMetrix::new(pango_context, self.line_space); 27 | } 28 | 29 | pub fn update_font_features(&mut self, font_features: FontFeatures) { 30 | self.font_features = font_features; 31 | } 32 | 33 | pub fn update_line_space(&mut self, line_space: i32) { 34 | self.line_space = line_space; 35 | let pango_context = self.font_metrics.pango_context.clone(); 36 | self.font_metrics = FontMetrix::new(pango_context, self.line_space); 37 | } 38 | 39 | pub fn itemize(&self, line: &StyledLine) -> Vec { 40 | let attr_iter = line.attr_list.get_iterator(); 41 | 42 | ItemizeIterator::new(&line.line_str) 43 | .flat_map(|(offset, len)| { 44 | pango::itemize( 45 | &self.font_metrics.pango_context, 46 | &line.line_str, 47 | offset as i32, 48 | len as i32, 49 | &line.attr_list, 50 | attr_iter.as_ref(), 51 | ) 52 | }) 53 | .collect() 54 | } 55 | 56 | pub fn create_layout(&self) -> pango::Layout { 57 | pango::Layout::new(&self.font_metrics.pango_context) 58 | } 59 | 60 | pub fn font_description(&self) -> &pango::FontDescription { 61 | &self.font_metrics.font_desc 62 | } 63 | 64 | pub fn cell_metrics(&self) -> &CellMetrics { 65 | &self.font_metrics.cell_metrics 66 | } 67 | 68 | pub fn font_features(&self) -> &FontFeatures { 69 | &self.font_features 70 | } 71 | 72 | pub fn font_families(&self) -> HashSet { 73 | self.font_metrics 74 | .pango_context 75 | .list_families() 76 | .iter() 77 | .filter_map(pango::FontFamilyExt::get_name) 78 | .collect() 79 | } 80 | } 81 | 82 | struct FontMetrix { 83 | pango_context: pango::Context, 84 | cell_metrics: CellMetrics, 85 | font_desc: pango::FontDescription, 86 | } 87 | 88 | impl FontMetrix { 89 | pub fn new(pango_context: pango::Context, line_space: i32) -> Self { 90 | let font_metrics = pango_context.get_metrics(None, None).unwrap(); 91 | let font_desc = pango_context.get_font_description().unwrap(); 92 | 93 | FontMetrix { 94 | pango_context, 95 | cell_metrics: CellMetrics::new(&font_metrics, line_space), 96 | font_desc, 97 | } 98 | } 99 | } 100 | 101 | pub struct CellMetrics { 102 | pub line_height: f64, 103 | pub char_width: f64, 104 | pub ascent: f64, 105 | pub underline_position: f64, 106 | pub underline_thickness: f64, 107 | pub strikethrough_position: f64, 108 | pub strikethrough_thickness: f64, 109 | pub pango_ascent: i32, 110 | pub pango_descent: i32, 111 | pub pango_char_width: i32, 112 | } 113 | 114 | impl CellMetrics { 115 | fn new(font_metrics: &pango::FontMetrics, line_space: i32) -> Self { 116 | let ascent = (f64::from(font_metrics.get_ascent()) / f64::from(pango::SCALE)).ceil(); 117 | let descent = (f64::from(font_metrics.get_descent()) / f64::from(pango::SCALE)).ceil(); 118 | 119 | // distance above top of underline, will typically be negative 120 | let pango_underline_position = f64::from(font_metrics.get_underline_position()); 121 | let underline_position = (pango_underline_position / f64::from(pango::SCALE)) 122 | .abs() 123 | .ceil() 124 | .copysign(pango_underline_position); 125 | 126 | let underline_thickness = 127 | (f64::from(font_metrics.get_underline_thickness()) / f64::from(pango::SCALE)).ceil(); 128 | 129 | let strikethrough_position = 130 | (f64::from(font_metrics.get_strikethrough_position()) / f64::from(pango::SCALE)).ceil(); 131 | let strikethrough_thickness = (f64::from(font_metrics.get_strikethrough_thickness()) 132 | / f64::from(pango::SCALE)) 133 | .ceil(); 134 | 135 | CellMetrics { 136 | pango_ascent: font_metrics.get_ascent(), 137 | pango_descent: font_metrics.get_descent(), 138 | pango_char_width: font_metrics.get_approximate_char_width(), 139 | ascent, 140 | line_height: ascent + descent + f64::from(line_space), 141 | char_width: f64::from(font_metrics.get_approximate_char_width()) 142 | / f64::from(pango::SCALE), 143 | underline_position: ascent - underline_position + underline_thickness / 2.0, 144 | underline_thickness, 145 | strikethrough_position: ascent - strikethrough_position + strikethrough_thickness / 2.0, 146 | strikethrough_thickness, 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | pub fn new_hw(line_height: f64, char_width: f64) -> Self { 152 | CellMetrics { 153 | pango_ascent: 0, 154 | pango_descent: 0, 155 | pango_char_width: 0, 156 | ascent: 0.0, 157 | line_height, 158 | char_width, 159 | underline_position: 0.0, 160 | underline_thickness: 0.0, 161 | strikethrough_position: 0.0, 162 | strikethrough_thickness: 0.0, 163 | } 164 | } 165 | } 166 | 167 | pub struct FontFeatures { 168 | attr: Option, 169 | } 170 | 171 | impl FontFeatures { 172 | pub fn new() -> Self { 173 | FontFeatures { attr: None } 174 | } 175 | 176 | pub fn from(font_features: String) -> Self { 177 | if font_features.trim().is_empty() { 178 | return Self::new(); 179 | } 180 | 181 | FontFeatures { 182 | attr: sys_pango::attribute::new_features(&font_features), 183 | } 184 | } 185 | 186 | pub fn insert_into(&self, attr_list: &pango::AttrList) { 187 | if let Some(ref attr) = self.attr { 188 | attr_list.insert(attr.clone()); 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/render/itemize.rs: -------------------------------------------------------------------------------- 1 | use std::str::CharIndices; 2 | 3 | pub struct ItemizeIterator<'a> { 4 | char_iter: CharIndices<'a>, 5 | line: &'a str, 6 | } 7 | 8 | impl<'a> ItemizeIterator<'a> { 9 | pub fn new(line: &'a str) -> Self { 10 | ItemizeIterator { 11 | char_iter: line.char_indices(), 12 | line, 13 | } 14 | } 15 | } 16 | 17 | impl<'a> Iterator for ItemizeIterator<'a> { 18 | type Item = (usize, usize); 19 | 20 | fn next(&mut self) -> Option { 21 | let mut start_index = None; 22 | 23 | let end_index = loop { 24 | if let Some((index, ch)) = self.char_iter.next() { 25 | let is_whitespace = ch.is_whitespace(); 26 | 27 | if start_index.is_none() && !is_whitespace { 28 | start_index = Some(index); 29 | } 30 | if start_index.is_some() && is_whitespace { 31 | break index; 32 | } 33 | } else { 34 | break self.line.len(); 35 | } 36 | }; 37 | 38 | if let Some(start_index) = start_index { 39 | Some((start_index, end_index - start_index)) 40 | } else { 41 | None 42 | } 43 | } 44 | } 45 | 46 | #[cfg(test)] 47 | mod tests { 48 | use super::*; 49 | 50 | #[test] 51 | fn test_iterator() { 52 | let mut iter = ItemizeIterator::new("Test line "); 53 | 54 | assert_eq!(Some((0, 4)), iter.next()); 55 | assert_eq!(Some((6, 4)), iter.next()); 56 | assert_eq!(None, iter.next()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/render/mod.rs: -------------------------------------------------------------------------------- 1 | mod context; 2 | mod itemize; 3 | mod model_clip_iterator; 4 | 5 | pub use self::context::CellMetrics; 6 | pub use self::context::{Context, FontFeatures}; 7 | use self::model_clip_iterator::{ModelClipIteratorFactory, RowView}; 8 | 9 | use crate::color; 10 | use crate::sys::pangocairo::*; 11 | use cairo; 12 | use pango; 13 | use pangocairo; 14 | 15 | use crate::cursor::{cursor_rect, Cursor}; 16 | use crate::highlight::HighlightMap; 17 | use crate::ui_model; 18 | 19 | trait ContextAlpha { 20 | fn set_source_rgbo(&self, _: &color::Color, _: Option); 21 | } 22 | 23 | impl ContextAlpha for cairo::Context { 24 | fn set_source_rgbo(&self, color: &color::Color, alpha: Option) { 25 | if let Some(alpha) = alpha { 26 | self.set_source_rgba(color.0, color.1, color.2, alpha); 27 | } else { 28 | self.set_source_rgb(color.0, color.1, color.2); 29 | } 30 | } 31 | } 32 | 33 | pub fn fill_background(ctx: &cairo::Context, hl: &HighlightMap, alpha: Option) { 34 | ctx.set_source_rgbo(hl.bg(), alpha); 35 | ctx.paint(); 36 | } 37 | 38 | pub fn render( 39 | ctx: &cairo::Context, 40 | cursor: &C, 41 | font_ctx: &context::Context, 42 | ui_model: &ui_model::UiModel, 43 | hl: &HighlightMap, 44 | bg_alpha: Option, 45 | ) { 46 | let cell_metrics = font_ctx.cell_metrics(); 47 | let &CellMetrics { char_width, .. } = cell_metrics; 48 | 49 | // draw background 50 | // disable antialiase for rectangle borders, so they will not be visible 51 | ctx.set_antialias(cairo::Antialias::None); 52 | for row_view in ui_model.get_clip_iterator(ctx, cell_metrics) { 53 | let mut line_x = 0.0; 54 | 55 | for (col, cell) in row_view.line.line.iter().enumerate() { 56 | draw_cell_bg(&row_view, hl, cell, col, line_x, bg_alpha); 57 | line_x += char_width; 58 | } 59 | } 60 | ctx.set_antialias(cairo::Antialias::Default); 61 | 62 | // draw text 63 | for row_view in ui_model.get_clip_iterator(ctx, cell_metrics) { 64 | let mut line_x = 0.0; 65 | 66 | for (col, cell) in row_view.line.line.iter().enumerate() { 67 | draw_cell(&row_view, hl, cell, col, line_x, 0.0); 68 | draw_underline_strikethrough(&row_view, hl, cell, line_x, 0.0); 69 | 70 | line_x += char_width; 71 | } 72 | } 73 | 74 | draw_cursor(ctx, cursor, font_ctx, ui_model, hl, bg_alpha); 75 | } 76 | 77 | fn draw_cursor( 78 | ctx: &cairo::Context, 79 | cursor: &C, 80 | font_ctx: &context::Context, 81 | ui_model: &ui_model::UiModel, 82 | hl: &HighlightMap, 83 | bg_alpha: Option, 84 | ) { 85 | let cell_metrics = font_ctx.cell_metrics(); 86 | let (cursor_row, cursor_col) = ui_model.get_cursor(); 87 | 88 | let (x1, y1, x2, y2) = ctx.clip_extents(); 89 | let line_x = cursor_col as f64 * cell_metrics.char_width; 90 | let line_y = cursor_row as f64 * cell_metrics.line_height; 91 | 92 | if line_x < x1 || line_y < y1 || line_x > x2 || line_y > y2 || !cursor.is_visible() { 93 | return; 94 | } 95 | 96 | let cell_metrics = font_ctx.cell_metrics(); 97 | let row_view = ui_model.get_row_view(ctx, cell_metrics, cursor_row); 98 | let cell_start_col = row_view.line.cell_to_item(cursor_col); 99 | 100 | if let Some(cursor_line) = ui_model.model().get(cursor_row) { 101 | let double_width = cursor_line 102 | .line 103 | .get(cursor_col + 1) 104 | .map_or(false, |c| c.double_width); 105 | 106 | if cell_start_col >= 0 { 107 | let cell = &cursor_line[cursor_col]; 108 | 109 | // clip cursor position 110 | let (clip_y, clip_width, clip_height) = 111 | cursor_rect(cursor.mode_info(), cell_metrics, line_y, double_width); 112 | ctx.rectangle(line_x, clip_y, clip_width, clip_height); 113 | ctx.clip(); 114 | 115 | // repaint cell backgound 116 | // disable antialiase for rectangle borders, so they will not be visible 117 | ctx.set_antialias(cairo::Antialias::None); 118 | ctx.set_operator(cairo::Operator::Source); 119 | fill_background(ctx, hl, bg_alpha); 120 | draw_cell_bg(&row_view, hl, cell, cursor_col, line_x, bg_alpha); 121 | ctx.set_antialias(cairo::Antialias::Default); 122 | 123 | // reapint cursor and text 124 | ctx.set_operator(cairo::Operator::Over); 125 | ctx.move_to(line_x, line_y); 126 | let cursor_alpha = cursor.draw(ctx, font_ctx, line_y, double_width, &hl); 127 | 128 | let cell_start_line_x = 129 | line_x - (cursor_col as i32 - cell_start_col) as f64 * cell_metrics.char_width; 130 | 131 | debug_assert!(cell_start_line_x >= 0.0); 132 | 133 | ctx.set_operator(cairo::Operator::Xor); 134 | draw_cell( 135 | &row_view, 136 | hl, 137 | cell, 138 | cell_start_col as usize, 139 | cell_start_line_x, 140 | cursor_alpha, 141 | ); 142 | draw_underline_strikethrough(&row_view, hl, cell, line_x, cursor_alpha); 143 | } else { 144 | ctx.move_to(line_x, line_y); 145 | cursor.draw(ctx, font_ctx, line_y, double_width, &hl); 146 | } 147 | } 148 | } 149 | 150 | fn draw_underline_strikethrough( 151 | cell_view: &RowView, 152 | hl: &HighlightMap, 153 | cell: &ui_model::Cell, 154 | line_x: f64, 155 | inverse_level: f64, 156 | ) { 157 | if cell.hl.underline || cell.hl.undercurl || cell.hl.strikethrough { 158 | let &RowView { 159 | ctx, 160 | line_y, 161 | cell_metrics: 162 | &CellMetrics { 163 | line_height, 164 | char_width, 165 | underline_position, 166 | underline_thickness, 167 | strikethrough_position, 168 | strikethrough_thickness, 169 | .. 170 | }, 171 | .. 172 | } = cell_view; 173 | 174 | if cell.hl.strikethrough { 175 | let fg = hl.actual_cell_fg(cell).inverse(inverse_level); 176 | ctx.set_source_rgb(fg.0, fg.1, fg.2); 177 | ctx.set_line_width(strikethrough_thickness); 178 | ctx.move_to(line_x, line_y + strikethrough_position); 179 | ctx.line_to(line_x + char_width, line_y + strikethrough_position); 180 | ctx.stroke(); 181 | } 182 | 183 | if cell.hl.undercurl { 184 | let sp = hl.actual_cell_sp(cell).inverse(inverse_level); 185 | ctx.set_source_rgba(sp.0, sp.1, sp.2, 0.7); 186 | 187 | let max_undercurl_height = (line_height - underline_position) * 2.0; 188 | let undercurl_height = (underline_thickness * 4.0).min(max_undercurl_height); 189 | let undercurl_y = line_y + underline_position - undercurl_height / 2.0; 190 | 191 | pangocairo::functions::show_error_underline( 192 | ctx, 193 | line_x, 194 | undercurl_y, 195 | char_width, 196 | undercurl_height, 197 | ); 198 | } else if cell.hl.underline { 199 | let fg = hl.actual_cell_fg(cell).inverse(inverse_level); 200 | ctx.set_source_rgb(fg.0, fg.1, fg.2); 201 | ctx.set_line_width(underline_thickness); 202 | ctx.move_to(line_x, line_y + underline_position); 203 | ctx.line_to(line_x + char_width, line_y + underline_position); 204 | ctx.stroke(); 205 | } 206 | } 207 | } 208 | 209 | fn draw_cell_bg( 210 | cell_view: &RowView, 211 | hl: &HighlightMap, 212 | cell: &ui_model::Cell, 213 | col: usize, 214 | line_x: f64, 215 | bg_alpha: Option, 216 | ) { 217 | let &RowView { 218 | ctx, 219 | line, 220 | line_y, 221 | cell_metrics: 222 | &CellMetrics { 223 | char_width, 224 | line_height, 225 | .. 226 | }, 227 | .. 228 | } = cell_view; 229 | 230 | let bg = hl.cell_bg(cell); 231 | 232 | if let Some(bg) = bg { 233 | if !line.is_binded_to_item(col) { 234 | if bg != hl.bg() { 235 | ctx.set_source_rgbo(bg, bg_alpha); 236 | ctx.rectangle(line_x, line_y, char_width, line_height); 237 | ctx.fill(); 238 | } 239 | } else { 240 | ctx.set_source_rgbo(bg, bg_alpha); 241 | ctx.rectangle( 242 | line_x, 243 | line_y, 244 | char_width * line.item_len_from_idx(col) as f64, 245 | line_height, 246 | ); 247 | ctx.fill(); 248 | } 249 | } 250 | } 251 | 252 | fn draw_cell( 253 | row_view: &RowView, 254 | hl: &HighlightMap, 255 | cell: &ui_model::Cell, 256 | col: usize, 257 | line_x: f64, 258 | inverse_level: f64, 259 | ) { 260 | let &RowView { 261 | ctx, 262 | line, 263 | line_y, 264 | cell_metrics: &CellMetrics { ascent, .. }, 265 | .. 266 | } = row_view; 267 | 268 | if let Some(item) = line.item_line[col].as_ref() { 269 | if let Some(ref glyphs) = item.glyphs { 270 | let fg = hl.actual_cell_fg(cell).inverse(inverse_level); 271 | 272 | ctx.move_to(line_x, line_y + ascent); 273 | ctx.set_source_rgb(fg.0, fg.1, fg.2); 274 | 275 | show_glyph_string(ctx, item.font(), glyphs); 276 | } 277 | } 278 | } 279 | 280 | pub fn shape_dirty(ctx: &context::Context, ui_model: &mut ui_model::UiModel, hl: &HighlightMap) { 281 | for line in ui_model.model_mut() { 282 | if !line.dirty_line { 283 | continue; 284 | } 285 | 286 | let styled_line = ui_model::StyledLine::from(line, hl, ctx.font_features()); 287 | let items = ctx.itemize(&styled_line); 288 | line.merge(&styled_line, &items); 289 | 290 | for (col, cell) in line.line.iter_mut().enumerate() { 291 | if cell.dirty { 292 | if let Some(item) = line.item_line[col].as_mut() { 293 | let mut glyphs = pango::GlyphString::new(); 294 | { 295 | let analysis = item.analysis(); 296 | let offset = item.item.offset() as usize; 297 | let length = item.item.length() as usize; 298 | if let Some(line_str) = styled_line.line_str.get(offset..offset + length) { 299 | pango::shape(&line_str, analysis, &mut glyphs); 300 | } else { 301 | warn!("Wrong itemize split"); 302 | } 303 | } 304 | 305 | item.set_glyphs(ctx, glyphs); 306 | } 307 | } 308 | 309 | cell.dirty = false; 310 | } 311 | 312 | line.dirty_line = false; 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/render/model_clip_iterator.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::min; 2 | use std::slice::Iter; 3 | 4 | use cairo; 5 | 6 | use super::context::CellMetrics; 7 | use crate::ui_model; 8 | 9 | pub struct RowView<'a> { 10 | pub line: &'a ui_model::Line, 11 | pub cell_metrics: &'a CellMetrics, 12 | pub line_y: f64, 13 | pub ctx: &'a cairo::Context, 14 | } 15 | 16 | impl<'a> RowView<'a> { 17 | pub fn new( 18 | row: usize, 19 | ctx: &'a cairo::Context, 20 | cell_metrics: &'a CellMetrics, 21 | line: &'a ui_model::Line, 22 | ) -> Self { 23 | RowView { 24 | line, 25 | line_y: row as f64 * cell_metrics.line_height, 26 | cell_metrics, 27 | ctx, 28 | } 29 | } 30 | } 31 | 32 | pub struct ModelClipIterator<'a> { 33 | model_idx: usize, 34 | model_iter: Iter<'a, ui_model::Line>, 35 | cell_metrics: &'a CellMetrics, 36 | ctx: &'a cairo::Context, 37 | } 38 | 39 | pub trait ModelClipIteratorFactory { 40 | fn get_clip_iterator<'a>( 41 | &'a self, 42 | ctx: &'a cairo::Context, 43 | cell_metrics: &'a CellMetrics, 44 | ) -> ModelClipIterator; 45 | 46 | fn get_row_view<'a>( 47 | &'a self, 48 | ctx: &'a cairo::Context, 49 | cell_metrics: &'a CellMetrics, 50 | col: usize, 51 | ) -> RowView<'a>; 52 | } 53 | 54 | impl<'a> Iterator for ModelClipIterator<'a> { 55 | type Item = RowView<'a>; 56 | 57 | fn next(&mut self) -> Option> { 58 | let next = if let Some(line) = self.model_iter.next() { 59 | Some(RowView::new( 60 | self.model_idx, 61 | self.ctx, 62 | self.cell_metrics, 63 | line, 64 | )) 65 | } else { 66 | None 67 | }; 68 | self.model_idx += 1; 69 | 70 | next 71 | } 72 | } 73 | 74 | /// Clip implemented as top - 1/bot + 1 75 | /// this is because in some cases(like 'g' character) drawing character does not fit to calculated bounds 76 | /// and if one line must be repainted - also previous and next line must be repainted to 77 | impl ModelClipIteratorFactory for ui_model::UiModel { 78 | fn get_row_view<'a>( 79 | &'a self, 80 | ctx: &'a cairo::Context, 81 | cell_metrics: &'a CellMetrics, 82 | col: usize, 83 | ) -> RowView<'a> { 84 | RowView::new(col, ctx, cell_metrics, &self.model()[col]) 85 | } 86 | 87 | fn get_clip_iterator<'a>( 88 | &'a self, 89 | ctx: &'a cairo::Context, 90 | cell_metrics: &'a CellMetrics, 91 | ) -> ModelClipIterator<'a> { 92 | let model = self.model(); 93 | 94 | let (x1, y1, x2, y2) = ctx.clip_extents(); 95 | 96 | // in case ctx.translate is used y1 can be less then 0 97 | // in this case just use 0 as top value 98 | let model_clip = ui_model::ModelRect::from_area(cell_metrics, x1, y1.max(0.0), x2, y2); 99 | 100 | let model_clip_top = if model_clip.top == 0 { 101 | 0 102 | } else { 103 | // looks like in some cases repaint can come from old model 104 | min(model.len() - 1, model_clip.top - 1) 105 | }; 106 | let model_clip_bot = min(model.len() - 1, model_clip.bot + 1); 107 | 108 | debug_assert!( 109 | model_clip_top <= model_clip_bot, 110 | "model line index starts at {} but ends at {}. model.len = {}", 111 | model_clip_top, 112 | model_clip_bot, 113 | model.len() 114 | ); 115 | 116 | ModelClipIterator { 117 | model_idx: model_clip_top, 118 | model_iter: model[model_clip_top..=model_clip_bot].iter(), 119 | ctx, 120 | cell_metrics, 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use std::rc::{Rc, Weak}; 2 | use std::cell::RefCell; 3 | 4 | use crate::shell::Shell; 5 | #[cfg(unix)] 6 | use gio; 7 | #[cfg(unix)] 8 | use gio::SettingsExt; 9 | 10 | #[derive(PartialEq)] 11 | pub enum FontSource { 12 | Rpc, 13 | #[cfg(unix)] 14 | Gnome, 15 | Default, 16 | } 17 | 18 | struct State { 19 | font_source: FontSource, 20 | 21 | #[cfg(unix)] 22 | gnome_interface_settings: gio::Settings, 23 | } 24 | 25 | impl State { 26 | #[cfg(unix)] 27 | pub fn new() -> State { 28 | State { 29 | font_source: FontSource::Default, 30 | gnome_interface_settings: gio::Settings::new("org.gnome.desktop.interface"), 31 | } 32 | } 33 | 34 | #[cfg(target_os = "windows")] 35 | pub fn new() -> State { 36 | State { font_source: FontSource::Default } 37 | } 38 | 39 | #[cfg(unix)] 40 | fn update_font(&mut self, shell: &mut Shell) { 41 | // rpc is priority for font 42 | if self.font_source == FontSource::Rpc { 43 | return; 44 | } 45 | 46 | if let Some(ref font_name) = 47 | self.gnome_interface_settings.get_string( 48 | "monospace-font-name", 49 | ) 50 | { 51 | shell.set_font_desc(font_name); 52 | self.font_source = FontSource::Gnome; 53 | } 54 | } 55 | } 56 | 57 | pub struct Settings { 58 | shell: Option>>, 59 | state: Rc>, 60 | } 61 | 62 | impl Settings { 63 | pub fn new() -> Settings { 64 | Settings { 65 | shell: None, 66 | state: Rc::new(RefCell::new(State::new())), 67 | } 68 | } 69 | 70 | pub fn set_shell(&mut self, shell: Weak>) { 71 | self.shell = Some(shell); 72 | } 73 | 74 | #[cfg(unix)] 75 | pub fn init(&mut self) { 76 | let shell = Weak::upgrade(self.shell.as_ref().unwrap()).unwrap(); 77 | let state = self.state.clone(); 78 | self.state.borrow_mut().update_font( 79 | &mut *shell.borrow_mut(), 80 | ); 81 | self.state 82 | .borrow() 83 | .gnome_interface_settings 84 | .connect_changed(move |_, _| { 85 | monospace_font_changed(&mut *shell.borrow_mut(), &mut *state.borrow_mut()) 86 | }); 87 | } 88 | 89 | #[cfg(target_os = "windows")] 90 | pub fn init(&mut self) {} 91 | 92 | pub fn set_font_source(&mut self, src: FontSource) { 93 | self.state.borrow_mut().font_source = src; 94 | } 95 | } 96 | 97 | #[cfg(unix)] 98 | fn monospace_font_changed(mut shell: &mut Shell, state: &mut State) { 99 | // rpc is priority for font 100 | if state.font_source != FontSource::Rpc { 101 | state.update_font(&mut shell); 102 | } 103 | } 104 | 105 | use std::path::Path; 106 | use std::fs::File; 107 | use std::io::prelude::*; 108 | 109 | use toml; 110 | use serde; 111 | 112 | use crate::dirs; 113 | 114 | pub trait SettingsLoader: Sized + serde::Serialize + Default { 115 | const SETTINGS_FILE: &'static str; 116 | 117 | fn from_str(s: &str) -> Result; 118 | 119 | fn load() -> Self { 120 | match load_err() { 121 | Ok(settings) => settings, 122 | Err(e) => { 123 | error!("{}", e); 124 | Default::default() 125 | } 126 | } 127 | } 128 | 129 | fn is_file_exists() -> bool { 130 | if let Ok(mut toml_path) = dirs::get_app_config_dir() { 131 | toml_path.push(Self::SETTINGS_FILE); 132 | toml_path.is_file() 133 | } else { 134 | false 135 | } 136 | } 137 | 138 | fn save(&self) { 139 | match save_err(self) { 140 | Ok(()) => (), 141 | Err(e) => error!("{}", e), 142 | } 143 | } 144 | } 145 | 146 | fn load_from_file(path: &Path) -> Result { 147 | if path.exists() { 148 | let mut file = File::open(path).map_err(|e| format!("{}", e))?; 149 | let mut contents = String::new(); 150 | file.read_to_string(&mut contents).map_err( 151 | |e| format!("{}", e), 152 | )?; 153 | T::from_str(&contents) 154 | } else { 155 | Ok(Default::default()) 156 | } 157 | } 158 | 159 | fn load_err() -> Result { 160 | let mut toml_path = dirs::get_app_config_dir_create()?; 161 | toml_path.push(T::SETTINGS_FILE); 162 | load_from_file(&toml_path) 163 | } 164 | 165 | 166 | fn save_err(sl: &T) -> Result<(), String> { 167 | let mut toml_path = dirs::get_app_config_dir_create()?; 168 | toml_path.push(T::SETTINGS_FILE); 169 | let mut file = File::create(toml_path).map_err(|e| format!("{}", e))?; 170 | 171 | let contents = toml::to_vec::(sl).map_err(|e| format!("{}", e))?; 172 | 173 | file.write_all(&contents).map_err(|e| format!("{}", e))?; 174 | 175 | Ok(()) 176 | } 177 | -------------------------------------------------------------------------------- /src/shell_dlg.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | 3 | use gtk; 4 | use gtk::prelude::*; 5 | use gtk::{ButtonsType, MessageDialog, MessageType}; 6 | 7 | use neovim_lib::{CallError, NeovimApi, Value}; 8 | use crate::shell::Shell; 9 | use crate::ui::{Components, UiMutex}; 10 | 11 | pub fn can_close_window(comps: &UiMutex, shell: &RefCell) -> bool { 12 | let shell = shell.borrow(); 13 | match get_changed_buffers(&*shell) { 14 | Ok(vec) => { 15 | if !vec.is_empty() { 16 | show_not_saved_dlg(comps, &*shell, &vec) 17 | } else { 18 | true 19 | } 20 | } 21 | Err(ref err) => { 22 | error!("Error getting info from nvim: {}", err); 23 | true 24 | } 25 | } 26 | } 27 | 28 | fn show_not_saved_dlg(comps: &UiMutex, shell: &Shell, changed_bufs: &[String]) -> bool { 29 | let mut changed_files = changed_bufs 30 | .iter() 31 | .map(|n| if n.is_empty() { "" } else { n }) 32 | .fold(String::new(), |acc, v| acc + v + "\n"); 33 | changed_files.pop(); 34 | 35 | let flags = gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT; 36 | let dlg = MessageDialog::new( 37 | Some(comps.borrow().window()), 38 | flags, 39 | MessageType::Question, 40 | ButtonsType::None, 41 | &format!("Save changes to '{}'?", changed_files), 42 | ); 43 | 44 | dlg.add_buttons(&[ 45 | ("_Yes", gtk::ResponseType::Yes), 46 | ("_No", gtk::ResponseType::No), 47 | ("_Cancel", gtk::ResponseType::Cancel), 48 | ]); 49 | 50 | let res = match dlg.run() { 51 | gtk::ResponseType::Yes => { 52 | let state = shell.state.borrow(); 53 | let mut nvim = state.nvim().unwrap(); 54 | match nvim.command("wa") { 55 | Err(ref err) => { 56 | error!("Error: {}", err); 57 | false 58 | } 59 | _ => true, 60 | } 61 | } 62 | gtk::ResponseType::No => true, 63 | gtk::ResponseType::Cancel | _ => false, 64 | }; 65 | 66 | dlg.destroy(); 67 | 68 | res 69 | } 70 | 71 | fn get_changed_buffers(shell: &Shell) -> Result, CallError> { 72 | let state = shell.state.borrow(); 73 | let nvim = state.nvim(); 74 | if let Some(mut nvim) = nvim { 75 | let buffers = nvim.list_bufs().unwrap(); 76 | 77 | Ok(buffers 78 | .iter() 79 | .map(|buf| { 80 | ( 81 | match buf.get_option(&mut nvim, "modified") { 82 | Ok(Value::Boolean(val)) => val, 83 | Ok(_) => { 84 | warn!("Value must be boolean"); 85 | false 86 | } 87 | Err(ref err) => { 88 | error!("Something going wrong while getting buffer option: {}", err); 89 | false 90 | } 91 | }, 92 | match buf.get_name(&mut nvim) { 93 | Ok(name) => name, 94 | Err(ref err) => { 95 | error!("Something going wrong while getting buffer name: {}", err); 96 | "".to_owned() 97 | } 98 | }, 99 | ) 100 | }) 101 | .filter(|e| e.0) 102 | .map(|e| e.1) 103 | .collect()) 104 | } else { 105 | Ok(vec![]) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/subscriptions.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use neovim_lib::{NeovimApi, NeovimApiAsync, Value}; 4 | 5 | use crate::nvim::{ErrorReport, NeovimRef}; 6 | 7 | /// A subscription to a Neovim autocmd event. 8 | struct Subscription { 9 | /// A callback to be executed each time the event triggers. 10 | cb: Box) + 'static>, 11 | /// A list of expressions which will be evaluated when the event triggers. The result is passed 12 | /// to the callback. 13 | args: Vec, 14 | } 15 | 16 | /// Subscription keys represent a NeoVim event coupled with a matching pattern. It is expected for 17 | /// the pattern more often than not to be `"*"`. 18 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 19 | pub struct SubscriptionKey { 20 | event_name: String, 21 | pattern: String, 22 | } 23 | 24 | impl<'a> From<&'a str> for SubscriptionKey { 25 | fn from(event_name: &'a str) -> Self { 26 | SubscriptionKey { 27 | event_name: event_name.to_owned(), 28 | pattern: "*".to_owned(), 29 | } 30 | } 31 | } 32 | 33 | impl SubscriptionKey { 34 | pub fn with_pattern(event_name: &str, pattern: &str) -> Self { 35 | SubscriptionKey { 36 | event_name: event_name.to_owned(), 37 | pattern: pattern.to_owned(), 38 | } 39 | } 40 | } 41 | 42 | /// A map of all registered subscriptions. 43 | pub struct Subscriptions(HashMap>); 44 | 45 | /// A handle to identify a `Subscription` within the `Subscriptions` map. 46 | /// 47 | /// Can be used to trigger the subscription manually even when the event was not triggered. 48 | /// 49 | /// Could be used in the future to suspend individual subscriptions. 50 | #[derive(Debug)] 51 | pub struct SubscriptionHandle { 52 | key: SubscriptionKey, 53 | index: usize, 54 | } 55 | 56 | impl Subscriptions { 57 | pub fn new() -> Self { 58 | Subscriptions(HashMap::new()) 59 | } 60 | 61 | /// Subscribe to a Neovim autocmd event. 62 | /// 63 | /// Subscriptions are not active immediately but only after `set_autocmds` is called. At the 64 | /// moment, all calls to `subscribe` must be made before calling `set_autocmds`. 65 | /// 66 | /// This function is wrapped by `shell::State`. 67 | /// 68 | /// # Arguments: 69 | /// 70 | /// - `key`: The subscription key to register. 71 | /// See `:help autocmd-events` for a list of supported event names. Event names can be 72 | /// comma-separated. 73 | /// 74 | /// - `args`: A list of expressions to be evaluated when the event triggers. 75 | /// Expressions are evaluated using Vimscript. The results are passed to the callback as a 76 | /// list of Strings. 77 | /// This is especially useful as `Neovim::eval` is synchronous and might block if called from 78 | /// the callback function; so always use the `args` mechanism instead. 79 | /// 80 | /// - `cb`: The callback function. 81 | /// This will be called each time the event triggers or when `run_now` is called. 82 | /// It is passed a vector with the results of the evaluated expressions given with `args`. 83 | /// 84 | /// # Example 85 | /// 86 | /// Call a function each time a buffer is entered or the current working directory is changed. 87 | /// Pass the current buffer name and directory to the callback. 88 | /// ``` 89 | /// let my_subscription = shell.state.borrow() 90 | /// .subscribe("BufEnter,DirChanged", &["expand(@%)", "getcwd()"], move |args| { 91 | /// let filename = &args[0]; 92 | /// let dir = &args[1]; 93 | /// // do stuff 94 | /// }); 95 | /// ``` 96 | pub fn subscribe(&mut self, key: SubscriptionKey, args: &[&str], cb: F) -> SubscriptionHandle 97 | where 98 | F: Fn(Vec) + 'static, 99 | { 100 | let entry = self.0.entry(key.clone()).or_insert_with(Vec::new); 101 | let index = entry.len(); 102 | entry.push(Subscription { 103 | cb: Box::new(cb), 104 | args: args.iter().map(|&s| s.to_owned()).collect(), 105 | }); 106 | SubscriptionHandle { key, index } 107 | } 108 | 109 | /// Register all subscriptions with Neovim. 110 | /// 111 | /// This function is wrapped by `shell::State`. 112 | pub fn set_autocmds(&self, nvim: &mut NeovimRef) { 113 | for (key, subscriptions) in &self.0 { 114 | let SubscriptionKey { 115 | event_name, 116 | pattern, 117 | } = key; 118 | for (i, subscription) in subscriptions.iter().enumerate() { 119 | let args = subscription 120 | .args 121 | .iter() 122 | .fold("".to_owned(), |acc, arg| acc + ", " + &arg); 123 | let autocmd = format!( 124 | "autocmd {} {} call rpcnotify(1, 'subscription', '{}', '{}', {} {})", 125 | event_name, pattern, event_name, pattern, i, args, 126 | ); 127 | nvim.command_async(&autocmd).cb(|r| r.report_err()).call(); 128 | } 129 | } 130 | } 131 | 132 | /// Trigger given event. 133 | fn on_notify(&self, key: &SubscriptionKey, index: usize, args: Vec) { 134 | if let Some(subscription) = self.0.get(key).and_then(|v| v.get(index)) { 135 | (*subscription.cb)(args); 136 | } 137 | } 138 | 139 | /// Wrapper around `on_notify` for easy calling with a `neovim_lib::Handler` implementation. 140 | /// 141 | /// This function is wrapped by `shell::State`. 142 | pub fn notify(&self, params: Vec) -> Result<(), String> { 143 | let mut params_iter = params.into_iter(); 144 | let ev_name = params_iter.next(); 145 | let ev_name = ev_name 146 | .as_ref() 147 | .and_then(Value::as_str) 148 | .ok_or("Error reading event name")?; 149 | let pattern = params_iter.next(); 150 | let pattern = pattern 151 | .as_ref() 152 | .and_then(Value::as_str) 153 | .ok_or("Error reading pattern")?; 154 | let key = SubscriptionKey { 155 | event_name: String::from(ev_name), 156 | pattern: String::from(pattern), 157 | }; 158 | let index = params_iter 159 | .next() 160 | .and_then(|i| i.as_u64()) 161 | .ok_or("Error reading index")? as usize; 162 | let args = params_iter 163 | .map(|arg| { 164 | arg.as_str() 165 | .map(str::to_owned) 166 | .or_else(|| arg.as_u64().map(|uint| uint.to_string())) 167 | }).collect::>>() 168 | .ok_or("Error reading args")?; 169 | self.on_notify(&key, index, args); 170 | Ok(()) 171 | } 172 | 173 | /// Manually trigger the given subscription. 174 | /// 175 | /// The `nvim` instance is needed to evaluate the `args` expressions. 176 | /// 177 | /// This function is wrapped by `shell::State`. 178 | pub fn run_now(&self, handle: &SubscriptionHandle, nvim: &mut NeovimRef) { 179 | let subscription = &self.0.get(&handle.key).unwrap()[handle.index]; 180 | let args = subscription 181 | .args 182 | .iter() 183 | .map(|arg| nvim.eval(arg)) 184 | .map(|res| { 185 | res.ok().and_then(|val| { 186 | val.as_str() 187 | .map(str::to_owned) 188 | .or_else(|| val.as_u64().map(|uint: u64| format!("{}", uint))) 189 | }) 190 | }).collect::>>(); 191 | if let Some(args) = args { 192 | self.on_notify(&handle.key, handle.index, args); 193 | } else { 194 | error!("Error manually running {:?}", handle); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/sys/mod.rs: -------------------------------------------------------------------------------- 1 | 2 | pub mod pango; 3 | pub mod pangocairo; 4 | -------------------------------------------------------------------------------- /src/sys/pango/attribute.rs: -------------------------------------------------------------------------------- 1 | use pango; 2 | use pango_sys; 3 | 4 | use glib::translate::*; 5 | 6 | pub fn new_features(features: &str) -> Option { 7 | unsafe { 8 | from_glib_full(pango_sys::pango_attr_font_features_new( 9 | features.to_glib_none().0, 10 | )) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/sys/pango/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod attribute; 2 | 3 | -------------------------------------------------------------------------------- /src/sys/pangocairo/mod.rs: -------------------------------------------------------------------------------- 1 | use pango; 2 | use cairo; 3 | 4 | use pango_cairo_sys as ffi; 5 | 6 | use glib::translate::*; 7 | 8 | pub fn show_glyph_string(cr: &cairo::Context, font: &pango::Font, glyphs: &pango::GlyphString) { 9 | unsafe { 10 | ffi::pango_cairo_show_glyph_string( 11 | mut_override(cr.to_glib_none().0), 12 | font.to_glib_none().0, 13 | mut_override(glyphs.to_glib_none().0), 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/tabline.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | use std::rc::Rc; 3 | use std::cell::RefCell; 4 | 5 | use gtk; 6 | use gtk::prelude::*; 7 | 8 | use glib; 9 | use glib::signal; 10 | 11 | use pango; 12 | 13 | use neovim_lib::{NeovimApi, NeovimApiAsync}; 14 | use neovim_lib::neovim_api::Tabpage; 15 | 16 | use crate::nvim; 17 | use crate::nvim::ErrorReport; 18 | 19 | struct State { 20 | data: Vec, 21 | selected: Option, 22 | nvim: Option>, 23 | } 24 | 25 | impl State { 26 | pub fn new() -> Self { 27 | State { 28 | data: Vec::new(), 29 | selected: None, 30 | nvim: None, 31 | } 32 | } 33 | 34 | fn switch_page(&self, idx: u32) { 35 | let target = &self.data[idx as usize]; 36 | if Some(target) != self.selected.as_ref() { 37 | if let Some(mut nvim) = self.nvim.as_ref().unwrap().nvim() { 38 | nvim.set_current_tabpage(target).report_err(); 39 | } 40 | } 41 | } 42 | 43 | fn close_tab(&self, idx: u32) { 44 | if let Some(mut nvim) = self.nvim.as_ref().unwrap().nvim() { 45 | nvim.command_async(&format!(":tabc {}", idx + 1)) 46 | .cb(|r| r.report_err()) 47 | .call(); 48 | } 49 | } 50 | } 51 | 52 | pub struct Tabline { 53 | tabs: gtk::Notebook, 54 | state: Rc>, 55 | switch_handler_id: glib::SignalHandlerId, 56 | } 57 | 58 | impl Tabline { 59 | pub fn new() -> Self { 60 | let tabs = gtk::Notebook::new(); 61 | 62 | tabs.set_can_focus(false); 63 | tabs.set_scrollable(true); 64 | tabs.set_show_border(false); 65 | tabs.set_border_width(0); 66 | tabs.set_hexpand(true); 67 | tabs.hide(); 68 | 69 | let state = Rc::new(RefCell::new(State::new())); 70 | 71 | let state_ref = state.clone(); 72 | let switch_handler_id = 73 | tabs.connect_switch_page(move |_, _, idx| state_ref.borrow().switch_page(idx)); 74 | 75 | Tabline { 76 | tabs, 77 | state, 78 | switch_handler_id, 79 | } 80 | } 81 | 82 | fn update_state( 83 | &self, 84 | nvim: &Rc, 85 | selected: &Tabpage, 86 | tabs: &[(Tabpage, Option)], 87 | ) { 88 | let mut state = self.state.borrow_mut(); 89 | 90 | if state.nvim.is_none() { 91 | state.nvim = Some(nvim.clone()); 92 | } 93 | 94 | state.selected = Some(selected.clone()); 95 | 96 | state.data = tabs.iter().map(|item| item.0.clone()).collect(); 97 | } 98 | 99 | pub fn update_tabs( 100 | &self, 101 | nvim: &Rc, 102 | selected: &Tabpage, 103 | tabs: &[(Tabpage, Option)], 104 | ) { 105 | if tabs.len() <= 1 { 106 | self.tabs.hide(); 107 | return; 108 | } else { 109 | self.tabs.show(); 110 | } 111 | 112 | self.update_state(nvim, selected, tabs); 113 | 114 | 115 | signal::signal_handler_block(&self.tabs, &self.switch_handler_id); 116 | 117 | let count = self.tabs.get_n_pages() as usize; 118 | if count < tabs.len() { 119 | for _ in count..tabs.len() { 120 | let empty = gtk::Box::new(gtk::Orientation::Vertical, 0); 121 | empty.show_all(); 122 | let title = gtk::Label::new(None); 123 | title.set_ellipsize(pango::EllipsizeMode::Middle); 124 | title.set_width_chars(25); 125 | let close_btn = gtk::Button::new_from_icon_name( 126 | Some("window-close-symbolic"), 127 | gtk::IconSize::Menu, 128 | ); 129 | close_btn.set_relief(gtk::ReliefStyle::None); 130 | close_btn.get_style_context().add_class("small-button"); 131 | close_btn.set_focus_on_click(false); 132 | let label_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); 133 | label_box.pack_start(&title, true, false, 0); 134 | label_box.pack_start(&close_btn, false, false, 0); 135 | title.show(); 136 | close_btn.show(); 137 | self.tabs.append_page(&empty, Some(&label_box)); 138 | self.tabs.set_child_tab_expand(&empty, true); 139 | 140 | let tabs = self.tabs.clone(); 141 | let state_ref = Rc::clone(&self.state); 142 | close_btn.connect_clicked(move |btn| { 143 | let current_label = btn 144 | .get_parent().unwrap(); 145 | for i in 0..tabs.get_n_pages() { 146 | let page = tabs.get_nth_page(Some(i)).unwrap(); 147 | let label = tabs.get_tab_label(&page).unwrap(); 148 | if label == current_label { 149 | state_ref.borrow().close_tab(i); 150 | } 151 | } 152 | }); 153 | } 154 | } else if count > tabs.len() { 155 | for _ in tabs.len()..count { 156 | self.tabs.remove_page(None); 157 | } 158 | } 159 | 160 | for (idx, tab) in tabs.iter().enumerate() { 161 | let tab_child = self.tabs.get_nth_page(Some(idx as u32)); 162 | let tab_label = self.tabs 163 | .get_tab_label(&tab_child.unwrap()) 164 | .unwrap() 165 | .downcast::() 166 | .unwrap() 167 | .get_children() 168 | .into_iter() 169 | .next() 170 | .unwrap() 171 | .downcast::() 172 | .unwrap(); 173 | tab_label.set_text(tab.1.as_ref().unwrap_or(&"??".to_owned())); 174 | 175 | if *selected == tab.0 { 176 | self.tabs.set_current_page(Some(idx as u32)); 177 | } 178 | } 179 | 180 | signal::signal_handler_unblock(&self.tabs, &self.switch_handler_id); 181 | } 182 | } 183 | 184 | impl Deref for Tabline { 185 | type Target = gtk::Notebook; 186 | 187 | fn deref(&self) -> >k::Notebook { 188 | &self.tabs 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/ui_model/cell.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::highlight::Highlight; 4 | 5 | #[derive(Clone)] 6 | pub struct Cell { 7 | pub hl: Rc, 8 | pub ch: String, 9 | pub dirty: bool, 10 | pub double_width: bool, 11 | } 12 | 13 | impl Cell { 14 | pub fn new_empty() -> Cell { 15 | Cell { 16 | hl: Rc::new(Highlight::new()), 17 | ch: String::new(), 18 | dirty: true, 19 | double_width: false, 20 | } 21 | } 22 | 23 | pub fn clear(&mut self, hl: Rc) { 24 | self.ch.clear(); 25 | self.hl = hl; 26 | self.dirty = true; 27 | self.double_width = false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/ui_model/item.rs: -------------------------------------------------------------------------------- 1 | use crate::render; 2 | 3 | use pango; 4 | 5 | #[derive(Clone)] 6 | pub struct Item { 7 | pub item: pango::Item, 8 | pub cells_count: usize, 9 | pub glyphs: Option, 10 | pub ink_overflow: Option, 11 | font: pango::Font, 12 | } 13 | 14 | impl Item { 15 | pub fn new(item: pango::Item, cells_count: usize) -> Self { 16 | debug_assert!(cells_count > 0); 17 | 18 | Item { 19 | font: item.analysis().font(), 20 | item, 21 | cells_count, 22 | glyphs: None, 23 | ink_overflow: None, 24 | } 25 | } 26 | 27 | pub fn update(&mut self, item: pango::Item) { 28 | self.font = item.analysis().font(); 29 | self.item = item; 30 | self.glyphs = None; 31 | self.ink_overflow = None; 32 | } 33 | 34 | pub fn set_glyphs(&mut self, ctx: &render::Context, glyphs: pango::GlyphString) { 35 | let mut glyphs = glyphs; 36 | let (ink_rect, _) = glyphs.extents(&self.font); 37 | self.ink_overflow = InkOverflow::from(ctx, &ink_rect, self.cells_count as i32); 38 | self.glyphs = Some(glyphs); 39 | } 40 | 41 | pub fn font(&self) -> &pango::Font { 42 | &self.font 43 | } 44 | 45 | pub fn analysis(&self) -> &pango::Analysis { 46 | self.item.analysis() 47 | } 48 | } 49 | 50 | #[derive(Clone)] 51 | pub struct InkOverflow { 52 | pub left: f64, 53 | pub right: f64, 54 | pub top: f64, 55 | pub bot: f64, 56 | } 57 | 58 | impl InkOverflow { 59 | pub fn from( 60 | ctx: &render::Context, 61 | ink_rect: &pango::Rectangle, 62 | cells_count: i32, 63 | ) -> Option { 64 | let cell_metrix = ctx.cell_metrics(); 65 | 66 | let ink_descent = ink_rect.y + ink_rect.height; 67 | let ink_ascent = ink_rect.y.abs(); 68 | 69 | let mut top = ink_ascent - cell_metrix.pango_ascent; 70 | if top < 0 { 71 | top = 0; 72 | } 73 | 74 | let mut bot = ink_descent - cell_metrix.pango_descent; 75 | if bot < 0 { 76 | bot = 0; 77 | } 78 | 79 | let left = if ink_rect.x < 0 { ink_rect.x.abs() } else { 0 }; 80 | 81 | let mut right = ink_rect.width - cells_count * cell_metrix.pango_char_width; 82 | if right < 0 { 83 | right = 0; 84 | } 85 | 86 | if left == 0 && right == 0 && top == 0 && bot == 0 { 87 | None 88 | } else { 89 | Some(InkOverflow { 90 | left: left as f64 / pango::SCALE as f64, 91 | right: right as f64 / pango::SCALE as f64, 92 | top: top as f64 / pango::SCALE as f64, 93 | bot: bot as f64 / pango::SCALE as f64, 94 | }) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/ui_model/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::highlight::Highlight; 2 | use std::rc::Rc; 3 | 4 | mod cell; 5 | mod item; 6 | mod line; 7 | mod model_layout; 8 | mod model_rect; 9 | 10 | pub use self::cell::Cell; 11 | pub use self::item::Item; 12 | pub use self::line::{Line, StyledLine}; 13 | pub use self::model_layout::ModelLayout; 14 | pub use self::model_rect::{ModelRect, ModelRectVec}; 15 | 16 | pub struct UiModel { 17 | pub columns: usize, 18 | pub rows: usize, 19 | cur_row: usize, 20 | cur_col: usize, 21 | model: Box<[Line]>, 22 | } 23 | 24 | impl UiModel { 25 | pub fn new(rows: u64, columns: u64) -> UiModel { 26 | let mut model = Vec::with_capacity(rows as usize); 27 | for _ in 0..rows as usize { 28 | model.push(Line::new(columns as usize)); 29 | } 30 | 31 | UiModel { 32 | columns: columns as usize, 33 | rows: rows as usize, 34 | cur_row: 0, 35 | cur_col: 0, 36 | model: model.into_boxed_slice(), 37 | } 38 | } 39 | 40 | pub fn empty() -> UiModel { 41 | UiModel { 42 | columns: 0, 43 | rows: 0, 44 | cur_row: 0, 45 | cur_col: 0, 46 | model: Box::new([]), 47 | } 48 | } 49 | 50 | #[inline] 51 | pub fn model(&self) -> &[Line] { 52 | &self.model 53 | } 54 | 55 | #[inline] 56 | pub fn model_mut(&mut self) -> &mut [Line] { 57 | &mut self.model 58 | } 59 | 60 | pub fn cur_point(&self) -> ModelRect { 61 | ModelRect::point(self.cur_col, self.cur_row) 62 | } 63 | 64 | pub fn set_cursor(&mut self, row: usize, col: usize) -> ModelRectVec { 65 | // it is possible in some cases that cursor moved out of visible rect 66 | // see https://github.com/daa84/neovim-gtk/issues/20 67 | if row >= self.model.len() || col >= self.model[row].line.len() { 68 | return ModelRectVec::empty(); 69 | } 70 | 71 | let mut changed_region = ModelRectVec::new(self.cur_point()); 72 | 73 | self.cur_row = row; 74 | self.cur_col = col; 75 | 76 | changed_region.join(&self.cur_point()); 77 | 78 | changed_region 79 | } 80 | 81 | pub fn get_cursor(&self) -> (usize, usize) { 82 | (self.cur_row, self.cur_col) 83 | } 84 | 85 | pub fn put_one(&mut self, row: usize, col: usize, ch: &str, double_width: bool, hl: Rc) { 86 | self.put(row, col, ch, double_width, 1, hl); 87 | } 88 | 89 | pub fn put( 90 | &mut self, 91 | row: usize, 92 | col: usize, 93 | ch: &str, 94 | double_width: bool, 95 | repeat: usize, 96 | hl: Rc, 97 | ) { 98 | let line = &mut self.model[row]; 99 | line.dirty_line = true; 100 | 101 | for offset in 0..repeat { 102 | let cell = &mut line[col + offset]; 103 | cell.ch.clear(); 104 | cell.ch.push_str(ch); 105 | cell.hl = hl.clone(); 106 | cell.double_width = double_width; 107 | cell.dirty = true; 108 | } 109 | } 110 | 111 | /// Copy rows from 0 to to_row, col from 0 self.columns 112 | /// 113 | /// Don't do any validation! 114 | pub fn swap_rows(&mut self, target: &mut UiModel, to_row: usize) { 115 | for (row_idx, line) in self.model[0..to_row + 1].iter_mut().enumerate() { 116 | let target_row = &mut target.model[row_idx]; 117 | line.swap_with(target_row, 0, self.columns - 1); 118 | } 119 | } 120 | 121 | #[inline] 122 | fn swap_row(&mut self, target_row: i64, offset: i64, left_col: usize, right_col: usize) { 123 | debug_assert_ne!(0, offset); 124 | 125 | let from_row = (target_row + offset) as usize; 126 | 127 | let (left, right) = if offset > 0 { 128 | self.model.split_at_mut(from_row) 129 | } else { 130 | self.model.split_at_mut(target_row as usize) 131 | }; 132 | 133 | let (source_row, target_row) = if offset > 0 { 134 | (&mut right[0], &mut left[target_row as usize]) 135 | } else { 136 | (&mut left[from_row], &mut right[0]) 137 | }; 138 | 139 | source_row.swap_with(target_row, left_col, right_col); 140 | } 141 | 142 | pub fn scroll(&mut self, top: i64, bot: i64, left: usize, right: usize, count: i64, default_hl: &Rc) -> ModelRect { 143 | if count > 0 { 144 | for row in top..(bot - count + 1) { 145 | self.swap_row(row, count, left, right); 146 | } 147 | } else { 148 | for row in ((top - count)..(bot + 1)).rev() { 149 | self.swap_row(row, count, left, right); 150 | } 151 | } 152 | 153 | if count > 0 { 154 | self.clear_region((bot - count + 1) as usize, bot as usize, left, right, default_hl); 155 | } else { 156 | self.clear_region(top as usize, (top - count - 1) as usize, left, right, default_hl); 157 | } 158 | 159 | ModelRect::new(top as usize, bot as usize, left, right) 160 | } 161 | 162 | pub fn clear(&mut self, default_hl: &Rc) { 163 | let (rows, columns) = (self.rows, self.columns); 164 | self.clear_region(0, rows - 1, 0, columns - 1, default_hl); 165 | } 166 | 167 | fn clear_region(&mut self, top: usize, bot: usize, left: usize, right: usize, default_hl: &Rc) { 168 | for row in &mut self.model[top..bot + 1] { 169 | row.clear(left, right, default_hl); 170 | } 171 | } 172 | 173 | pub fn clear_glyphs(&mut self) { 174 | for row in &mut self.model.iter_mut() { 175 | row.clear_glyphs(); 176 | } 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | mod tests { 182 | use super::*; 183 | 184 | #[test] 185 | fn test_vec_join_inside() { 186 | let mut list = ModelRectVec::new(ModelRect::new(0, 23, 0, 69)); 187 | 188 | let inside = ModelRect::new(23, 23, 68, 69); 189 | 190 | list.join(&inside); 191 | assert_eq!(1, list.list.len()); 192 | } 193 | 194 | #[test] 195 | fn test_vec_join_top() { 196 | let mut list = ModelRectVec::new(ModelRect::point(0, 0)); 197 | 198 | let neighbor = ModelRect::point(1, 0); 199 | 200 | list.join(&neighbor); 201 | assert_eq!(1, list.list.len()); 202 | } 203 | 204 | #[test] 205 | fn test_model_vec_join_right() { 206 | let mut list = ModelRectVec::new(ModelRect::new(23, 23, 69, 69)); 207 | 208 | let neighbor = ModelRect::new(23, 23, 69, 70); 209 | 210 | list.join(&neighbor); 211 | assert_eq!(1, list.list.len()); 212 | } 213 | 214 | #[test] 215 | fn test_model_vec_join_right2() { 216 | let mut list = ModelRectVec::new(ModelRect::new(0, 1, 0, 9)); 217 | 218 | let neighbor = ModelRect::new(1, 1, 9, 10); 219 | 220 | list.join(&neighbor); 221 | assert_eq!(1, list.list.len()); 222 | } 223 | 224 | #[test] 225 | fn test_model_vec_join() { 226 | let mut list = ModelRectVec::new(ModelRect::point(5, 5)); 227 | 228 | let neighbor = ModelRect::point(6, 5); 229 | 230 | list.join(&neighbor); 231 | assert_eq!(1, list.list.len()); 232 | } 233 | 234 | #[test] 235 | fn test_model_vec_no_join() { 236 | let mut list = ModelRectVec::new(ModelRect::point(5, 5)); 237 | 238 | let not_neighbor = ModelRect::point(6, 6); 239 | 240 | list.join(¬_neighbor); 241 | assert_eq!(2, list.list.len()); 242 | } 243 | 244 | #[test] 245 | fn test_cursor_area() { 246 | let mut model = UiModel::new(10, 20); 247 | 248 | model.set_cursor(1, 1); 249 | 250 | let rect = model.set_cursor(5, 5); 251 | 252 | assert_eq!(2, rect.list.len()); 253 | 254 | assert_eq!(1, rect.list[0].top); 255 | assert_eq!(1, rect.list[0].left); 256 | assert_eq!(1, rect.list[0].bot); 257 | assert_eq!(1, rect.list[0].right); 258 | 259 | assert_eq!(5, rect.list[1].top); 260 | assert_eq!(5, rect.list[1].left); 261 | assert_eq!(5, rect.list[1].bot); 262 | assert_eq!(5, rect.list[1].right); 263 | } 264 | 265 | #[test] 266 | fn test_scroll_area() { 267 | let mut model = UiModel::new(10, 20); 268 | 269 | let rect = model.scroll(1, 5, 1, 5, 3, &Rc::new(Highlight::new())); 270 | 271 | assert_eq!(1, rect.top); 272 | assert_eq!(1, rect.left); 273 | assert_eq!(5, rect.bot); 274 | assert_eq!(5, rect.right); 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/ui_model/model_layout.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::max; 2 | use std::rc::Rc; 3 | 4 | use unicode_width::UnicodeWidthStr; 5 | 6 | use crate::highlight::Highlight; 7 | use crate::ui_model::UiModel; 8 | 9 | pub struct ModelLayout { 10 | pub model: UiModel, 11 | rows_filled: usize, 12 | cols_filled: usize, 13 | lines: Vec, Vec)>>, 14 | } 15 | 16 | impl ModelLayout { 17 | const ROWS_STEP: usize = 10; 18 | 19 | pub fn new(columns: u64) -> Self { 20 | ModelLayout { 21 | model: UiModel::new(ModelLayout::ROWS_STEP as u64, columns), 22 | rows_filled: 0, 23 | cols_filled: 0, 24 | lines: Vec::new(), 25 | } 26 | } 27 | 28 | pub fn layout_append(&mut self, mut lines: Vec, Vec)>>) { 29 | let rows_filled = self.rows_filled; 30 | let take_from = self.lines.len(); 31 | 32 | self.lines.append(&mut lines); 33 | 34 | self.layout_replace(rows_filled, take_from); 35 | } 36 | 37 | pub fn layout(&mut self, lines: Vec, Vec)>>) { 38 | self.lines = lines; 39 | self.layout_replace(0, 0); 40 | } 41 | 42 | pub fn set_cursor(&mut self, col: usize) { 43 | let row = if self.rows_filled > 0 { 44 | self.rows_filled - 1 45 | } else { 46 | 0 47 | }; 48 | 49 | self.model.set_cursor(row, col); 50 | } 51 | 52 | pub fn size(&self) -> (usize, usize) { 53 | ( 54 | max(self.cols_filled, self.model.get_cursor().1 + 1), 55 | self.rows_filled, 56 | ) 57 | } 58 | 59 | fn check_model_size(&mut self, rows: usize) { 60 | if rows > self.model.rows { 61 | let model_cols = self.model.columns; 62 | let model_rows = ((rows / (ModelLayout::ROWS_STEP + 1)) + 1) * ModelLayout::ROWS_STEP; 63 | let (cur_row, cur_col) = self.model.get_cursor(); 64 | 65 | let mut model = UiModel::new(model_rows as u64, model_cols as u64); 66 | self.model.swap_rows(&mut model, self.rows_filled - 1); 67 | model.set_cursor(cur_row, cur_col); 68 | self.model = model; 69 | } 70 | } 71 | 72 | pub fn insert_char(&mut self, ch: String, shift: bool, hl: Rc) { 73 | if ch.is_empty() { 74 | return; 75 | } 76 | 77 | let (row, col) = self.model.get_cursor(); 78 | 79 | if shift { 80 | self.insert_into_lines(ch); 81 | self.layout_replace(0, 0); 82 | } else { 83 | self.model.put_one(row, col, &ch, false, hl); 84 | } 85 | } 86 | 87 | fn insert_into_lines(&mut self, ch: String) { 88 | let line = &mut self.lines[0]; 89 | 90 | let cur_col = self.model.cur_col; 91 | 92 | let mut col_idx = 0; 93 | for &mut (_, ref mut chars) in line { 94 | if cur_col < col_idx + chars.len() { 95 | let col_sub_idx = cur_col - col_idx; 96 | chars.insert(col_sub_idx, ch); 97 | break; 98 | } else { 99 | col_idx += chars.len(); 100 | } 101 | } 102 | } 103 | 104 | /// Wrap all lines into model 105 | /// 106 | /// returns actual width 107 | fn layout_replace(&mut self, row_offset: usize, take_from: usize) { 108 | let rows = ModelLayout::count_lines(&self.lines[take_from..], self.model.columns); 109 | 110 | self.check_model_size(rows + row_offset); 111 | self.rows_filled = rows + row_offset; 112 | 113 | let lines = &self.lines[take_from..]; 114 | 115 | let mut max_col_idx = 0; 116 | let mut col_idx = 0; 117 | let mut row_idx = row_offset; 118 | for content in lines { 119 | for &(ref hl, ref ch_list) in content { 120 | for ch in ch_list { 121 | let ch_width = max(1, ch.width()); 122 | 123 | if col_idx + ch_width > self.model.columns { 124 | col_idx = 0; 125 | row_idx += 1; 126 | } 127 | 128 | self.model.put_one(row_idx, col_idx, ch, false, hl.clone()); 129 | if ch_width > 1 { 130 | self.model.put_one(row_idx, col_idx, "", true, hl.clone()); 131 | } 132 | 133 | if max_col_idx < col_idx { 134 | max_col_idx = col_idx + ch_width - 1; 135 | } 136 | 137 | col_idx += ch_width; 138 | } 139 | 140 | if col_idx < self.model.columns { 141 | self.model.model[row_idx].clear( 142 | col_idx, 143 | self.model.columns - 1, 144 | &Rc::new(Highlight::new()), 145 | ); 146 | } 147 | } 148 | col_idx = 0; 149 | row_idx += 1; 150 | } 151 | 152 | if self.rows_filled == 1 { 153 | self.cols_filled = max_col_idx + 1; 154 | } else { 155 | self.cols_filled = max(self.cols_filled, max_col_idx + 1); 156 | } 157 | } 158 | 159 | fn count_lines(lines: &[Vec<(Rc, Vec)>], max_columns: usize) -> usize { 160 | let mut row_count = 0; 161 | 162 | for line in lines { 163 | let len: usize = line.iter().map(|c| c.1.len()).sum(); 164 | row_count += len / (max_columns + 1) + 1; 165 | } 166 | 167 | row_count 168 | } 169 | } 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use super::*; 174 | 175 | #[test] 176 | fn test_count_lines() { 177 | let lines = vec![vec![(Rc::new(Highlight::new()), vec!["a".to_owned(); 5])]]; 178 | 179 | let rows = ModelLayout::count_lines(&lines, 4); 180 | assert_eq!(2, rows); 181 | } 182 | 183 | #[test] 184 | fn test_resize() { 185 | let lines = vec![ 186 | vec![(Rc::new(Highlight::new()), vec!["a".to_owned(); 5])]; 187 | ModelLayout::ROWS_STEP 188 | ]; 189 | let mut model = ModelLayout::new(5); 190 | 191 | model.layout(lines.clone()); 192 | let (cols, rows) = model.size(); 193 | assert_eq!(5, cols); 194 | assert_eq!(ModelLayout::ROWS_STEP, rows); 195 | 196 | model.layout_append(lines); 197 | let (cols, rows) = model.size(); 198 | assert_eq!(5, cols); 199 | assert_eq!(ModelLayout::ROWS_STEP * 2, rows); 200 | assert_eq!(ModelLayout::ROWS_STEP * 2, model.model.rows); 201 | } 202 | 203 | #[test] 204 | fn test_cols_filled() { 205 | let lines = vec![vec![(Rc::new(Highlight::new()), vec!["a".to_owned(); 3])]; 1]; 206 | let mut model = ModelLayout::new(5); 207 | 208 | model.layout(lines); 209 | // cursor is not moved by newgrid api 210 | // so set it manual 211 | model.set_cursor(3); 212 | let (cols, _) = model.size(); 213 | assert_eq!(4, cols); // size is 3 and 4 - is with cursor position 214 | 215 | let lines = vec![vec![(Rc::new(Highlight::new()), vec!["a".to_owned(); 2])]; 1]; 216 | 217 | model.layout_append(lines); 218 | model.set_cursor(2); 219 | let (cols, _) = model.size(); 220 | assert_eq!(3, cols); 221 | } 222 | 223 | #[test] 224 | fn test_insert_shift() { 225 | let lines = vec![vec![(Rc::new(Highlight::new()), vec!["a".to_owned(); 3])]; 1]; 226 | let mut model = ModelLayout::new(5); 227 | model.layout(lines); 228 | model.set_cursor(1); 229 | 230 | model.insert_char("b".to_owned(), true, Rc::new(Highlight::new())); 231 | 232 | let (cols, _) = model.size(); 233 | assert_eq!(4, cols); 234 | assert_eq!("b", model.model.model()[0].line[1].ch); 235 | } 236 | 237 | #[test] 238 | fn test_insert_no_shift() { 239 | let lines = vec![vec![(Rc::new(Highlight::new()), vec!["a".to_owned(); 3])]; 1]; 240 | let mut model = ModelLayout::new(5); 241 | model.layout(lines); 242 | model.set_cursor(1); 243 | 244 | model.insert_char("b".to_owned(), false, Rc::new(Highlight::new())); 245 | 246 | let (cols, _) = model.size(); 247 | assert_eq!(3, cols); 248 | assert_eq!("b", model.model.model()[0].line[1].ch); 249 | } 250 | 251 | #[test] 252 | fn test_double_width() { 253 | let lines = vec![vec![(Rc::new(Highlight::new()), vec!["あ".to_owned(); 3])]; 1]; 254 | let mut model = ModelLayout::new(7); 255 | model.layout(lines); 256 | model.set_cursor(1); 257 | 258 | let (cols, rows) = model.size(); 259 | assert_eq!(1, rows); 260 | assert_eq!(6, cols); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /src/ui_model/model_rect.rs: -------------------------------------------------------------------------------- 1 | use super::item::Item; 2 | use super::UiModel; 3 | use crate::render::CellMetrics; 4 | 5 | #[derive(Clone, Debug)] 6 | pub struct ModelRectVec { 7 | pub list: Vec, 8 | } 9 | 10 | impl ModelRectVec { 11 | pub fn empty() -> ModelRectVec { 12 | ModelRectVec { list: vec![] } 13 | } 14 | 15 | pub fn new(first: ModelRect) -> ModelRectVec { 16 | ModelRectVec { list: vec![first] } 17 | } 18 | 19 | fn find_neighbor(&self, neighbor: &ModelRect) -> Option { 20 | for (i, rect) in self.list.iter().enumerate() { 21 | if (neighbor.top > 0 && rect.top == neighbor.top - 1 || rect.bot == neighbor.bot + 1) 22 | && neighbor.in_horizontal(rect) 23 | { 24 | return Some(i); 25 | } else if (neighbor.left > 0 && rect.left == neighbor.left - 1 26 | || rect.right == neighbor.right + 1) 27 | && neighbor.in_vertical(rect) 28 | { 29 | return Some(i); 30 | } else if rect.in_horizontal(neighbor) && rect.in_vertical(neighbor) { 31 | return Some(i); 32 | } else if rect.contains(neighbor) { 33 | return Some(i); 34 | } 35 | } 36 | 37 | None 38 | } 39 | 40 | pub fn join(&mut self, other: &ModelRect) { 41 | match self.find_neighbor(other) { 42 | Some(i) => self.list[i].join(other), 43 | None => self.list.push(other.clone()), 44 | } 45 | } 46 | } 47 | 48 | #[derive(Clone, PartialEq, Debug)] 49 | pub struct ModelRect { 50 | pub top: usize, 51 | pub bot: usize, 52 | pub left: usize, 53 | pub right: usize, 54 | } 55 | 56 | impl ModelRect { 57 | pub fn new(top: usize, bot: usize, left: usize, right: usize) -> ModelRect { 58 | debug_assert!(top <= bot, "{} <= {}", top, bot); 59 | debug_assert!(left <= right, "{} <= {}", left, right); 60 | 61 | ModelRect { 62 | top, 63 | bot, 64 | left, 65 | right, 66 | } 67 | } 68 | 69 | pub fn point(x: usize, y: usize) -> ModelRect { 70 | ModelRect { 71 | top: y, 72 | bot: y, 73 | left: x, 74 | right: x, 75 | } 76 | } 77 | 78 | #[inline] 79 | fn in_horizontal(&self, other: &ModelRect) -> bool { 80 | other.left >= self.left && other.left <= self.right 81 | || other.right >= self.left && other.right >= self.right 82 | } 83 | 84 | #[inline] 85 | fn in_vertical(&self, other: &ModelRect) -> bool { 86 | other.top >= self.top && other.top <= self.bot 87 | || other.bot >= self.top && other.bot <= self.bot 88 | } 89 | 90 | fn contains(&self, other: &ModelRect) -> bool { 91 | self.top <= other.top 92 | && self.bot >= other.bot 93 | && self.left <= other.left 94 | && self.right >= other.right 95 | } 96 | 97 | /// Extend rect to left and right to make changed Item rerendered 98 | pub fn extend_by_items(&mut self, model: Option<&UiModel>) { 99 | if model.is_none() { 100 | return; 101 | } 102 | let model = model.unwrap(); 103 | 104 | let mut left = self.left; 105 | let mut right = self.right; 106 | 107 | for i in self.top..self.bot + 1 { 108 | let line = &model.model[i]; 109 | let item_idx = line.cell_to_item(self.left); 110 | if item_idx >= 0 { 111 | let item_idx = item_idx as usize; 112 | if item_idx < left { 113 | left = item_idx; 114 | } 115 | } 116 | 117 | let len_since_right = line.item_len_from_idx(self.right) - 1; 118 | if right < self.right + len_since_right { 119 | right = self.right + len_since_right; 120 | } 121 | 122 | // extend also double_width chars 123 | let cell = &line.line[self.left]; 124 | if self.left > 0 && cell.double_width { 125 | let dw_char_idx = self.left - 1; 126 | if dw_char_idx < left { 127 | left = dw_char_idx; 128 | } 129 | } 130 | 131 | let dw_char_idx = self.right + 1; 132 | if let Some(cell) = line.line.get(dw_char_idx) { 133 | if cell.double_width { 134 | if right < dw_char_idx { 135 | right = dw_char_idx; 136 | } 137 | } 138 | } 139 | } 140 | 141 | self.left = left; 142 | self.right = right; 143 | } 144 | 145 | pub fn to_area_extend_ink( 146 | &self, 147 | model: Option<&UiModel>, 148 | cell_metrics: &CellMetrics, 149 | ) -> (i32, i32, i32, i32) { 150 | let (x, x2) = self.extend_left_right_area(model, cell_metrics); 151 | let (y, y2) = self.extend_top_bottom_area(model, cell_metrics); 152 | 153 | (x, y, x2 - x, y2 - y) 154 | } 155 | 156 | fn extend_left_right_area(&self, model: Option<&UiModel>, cell_metrics: &CellMetrics) -> (i32, i32) { 157 | // when convert to i32 area must be bigger then original f64 version 158 | let x = (self.left as f64 * cell_metrics.char_width).floor() as i32; 159 | let x2 = ((self.right + 1) as f64 * cell_metrics.char_width).ceil() as i32; 160 | 161 | if model.is_none() { 162 | return (x, x2); 163 | } 164 | let model = model.unwrap(); 165 | 166 | let mut min_x_offset = 0.0; 167 | let mut max_x_offset = 0.0; 168 | 169 | for row in self.top..self.bot + 1 { 170 | // left 171 | let line = &model.model[row]; 172 | if let Some(&Item { 173 | ink_overflow: Some(ref overflow), 174 | .. 175 | }) = line.item_line[self.left].as_ref() 176 | { 177 | if min_x_offset < overflow.left { 178 | min_x_offset = overflow.left; 179 | } 180 | } 181 | 182 | // right 183 | let line = &model.model[row]; 184 | // check if this item ends here 185 | if self.right < model.columns - 1 186 | && line.cell_to_item(self.right) != line.cell_to_item(self.right + 1) 187 | { 188 | if let Some(&Item { 189 | ink_overflow: Some(ref overflow), 190 | .. 191 | }) = line.get_item(self.left) 192 | { 193 | if max_x_offset < overflow.right { 194 | max_x_offset = overflow.right; 195 | } 196 | } 197 | } 198 | } 199 | 200 | ( 201 | x - min_x_offset.ceil() as i32, 202 | x2 + max_x_offset.ceil() as i32, 203 | ) 204 | } 205 | 206 | fn extend_top_bottom_area(&self, model: Option<&UiModel>, cell_metrics: &CellMetrics) -> (i32, i32) { 207 | let y = self.top as i32 * cell_metrics.line_height as i32; 208 | let y2 = (self.bot + 1) as i32 * cell_metrics.line_height as i32; 209 | 210 | if model.is_none() { 211 | return (y, y2); 212 | } 213 | let model = model.unwrap(); 214 | 215 | let mut min_y_offset = 0.0; 216 | let mut max_y_offset = 0.0; 217 | 218 | for col in self.left..self.right + 1 { 219 | // top 220 | let line = &model.model[self.top]; 221 | if let Some(&Item { 222 | ink_overflow: Some(ref overflow), 223 | .. 224 | }) = line.get_item(col) 225 | { 226 | if min_y_offset < overflow.top { 227 | min_y_offset = overflow.top; 228 | } 229 | } 230 | 231 | // bottom 232 | let line = &model.model[self.bot]; 233 | if let Some(&Item { 234 | ink_overflow: Some(ref overflow), 235 | .. 236 | }) = line.get_item(col) 237 | { 238 | if max_y_offset < overflow.top { 239 | max_y_offset = overflow.top; 240 | } 241 | } 242 | } 243 | 244 | ( 245 | y - min_y_offset.ceil() as i32, 246 | y2 + max_y_offset.ceil() as i32, 247 | ) 248 | } 249 | 250 | pub fn join(&mut self, rect: &ModelRect) { 251 | self.top = if self.top < rect.top { 252 | self.top 253 | } else { 254 | rect.top 255 | }; 256 | self.left = if self.left < rect.left { 257 | self.left 258 | } else { 259 | rect.left 260 | }; 261 | 262 | self.bot = if self.bot > rect.bot { 263 | self.bot 264 | } else { 265 | rect.bot 266 | }; 267 | self.right = if self.right > rect.right { 268 | self.right 269 | } else { 270 | rect.right 271 | }; 272 | 273 | debug_assert!(self.top <= self.bot); 274 | debug_assert!(self.left <= self.right); 275 | } 276 | 277 | pub fn to_area(&self, cell_metrics: &CellMetrics) -> (i32, i32, i32, i32) { 278 | let &CellMetrics { 279 | char_width, 280 | line_height, 281 | .. 282 | } = cell_metrics; 283 | 284 | // when convert to i32 area must be bigger then original f64 version 285 | ( 286 | (self.left as f64 * char_width).floor() as i32, 287 | (self.top as f64 * line_height).floor() as i32, 288 | ((self.right - self.left + 1) as f64 * char_width).ceil() as i32, 289 | ((self.bot - self.top + 1) as f64 * line_height).ceil() as i32, 290 | ) 291 | } 292 | 293 | pub fn from_area(cell_metrics: &CellMetrics, x1: f64, y1: f64, x2: f64, y2: f64) -> ModelRect { 294 | let &CellMetrics { 295 | char_width, 296 | line_height, 297 | .. 298 | } = cell_metrics; 299 | 300 | let x2 = if x2 > 0.0 { x2 - 1.0 } else { x2 }; 301 | let y2 = if y2 > 0.0 { y2 - 1.0 } else { y2 }; 302 | let left = (x1 / char_width) as usize; 303 | let right = (x2 / char_width) as usize; 304 | let top = (y1 / line_height) as usize; 305 | let bot = (y2 / line_height) as usize; 306 | 307 | ModelRect::new(top, bot, left, right) 308 | } 309 | } 310 | 311 | impl AsRef for ModelRect { 312 | fn as_ref(&self) -> &ModelRect { 313 | self 314 | } 315 | } 316 | 317 | #[cfg(test)] 318 | mod tests { 319 | use super::*; 320 | 321 | #[test] 322 | fn test_repaint_rect() { 323 | let rect = ModelRect::point(1, 1); 324 | let (x, y, width, height) = rect.to_area(&CellMetrics::new_hw(10.0, 5.0)); 325 | 326 | assert_eq!(5, x); 327 | assert_eq!(10, y); 328 | assert_eq!(5, width); 329 | assert_eq!(10, height); 330 | } 331 | 332 | #[test] 333 | fn test_from_area() { 334 | let rect = ModelRect::from_area(&CellMetrics::new_hw(10.0, 5.0), 3.0, 3.0, 9.0, 17.0); 335 | 336 | assert_eq!(0, rect.top); 337 | assert_eq!(0, rect.left); 338 | assert_eq!(1, rect.bot); 339 | assert_eq!(1, rect.right); 340 | 341 | let rect = ModelRect::from_area(&CellMetrics::new_hw(10.0, 5.0), 0.0, 0.0, 10.0, 20.0); 342 | 343 | assert_eq!(0, rect.top); 344 | assert_eq!(0, rect.left); 345 | assert_eq!(1, rect.bot); 346 | assert_eq!(1, rect.right); 347 | 348 | let rect = ModelRect::from_area(&CellMetrics::new_hw(10.0, 5.0), 0.0, 0.0, 11.0, 21.0); 349 | 350 | assert_eq!(0, rect.top); 351 | assert_eq!(0, rect.left); 352 | assert_eq!(2, rect.bot); 353 | assert_eq!(2, rect.right); 354 | } 355 | 356 | } 357 | -------------------------------------------------------------------------------- /src/value.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use neovim_lib::Value; 3 | 4 | pub trait ValueMapExt { 5 | fn to_attrs_map(&self) -> Result, String>; 6 | 7 | fn to_attrs_map_report(&self) -> Option>; 8 | } 9 | 10 | impl ValueMapExt for Vec<(Value, Value)> { 11 | fn to_attrs_map(&self) -> Result, String> { 12 | self.iter() 13 | .map(|p| { 14 | p.0 15 | .as_str() 16 | .ok_or_else(|| "Can't convert map key to string".to_owned()) 17 | .map(|key| (key, &p.1)) 18 | }) 19 | .collect::, String>>() 20 | 21 | } 22 | 23 | fn to_attrs_map_report(&self) -> Option> { 24 | match self.to_attrs_map() { 25 | Err(e) => { 26 | error!("{}", e); 27 | None 28 | } 29 | Ok(m) => Some(m), 30 | } 31 | } 32 | } 33 | --------------------------------------------------------------------------------