├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── build.rs ├── com.github.Lyude.neovim-gtk.yaml ├── desktop ├── com.github.Lyude.neovim-gtk-symbolic.svg ├── com.github.Lyude.neovim-gtk.desktop ├── com.github.Lyude.neovim-gtk.metainfo.xml ├── com.github.Lyude.neovim-gtk.svg ├── com.github.Lyude.neovim-gtk_128.png ├── com.github.Lyude.neovim-gtk_48.png └── dejavu_font │ ├── DejaVu Fonts License.txt │ ├── DejaVuSansMono-Bold.ttf │ ├── DejaVuSansMono-BoldOblique.ttf │ ├── DejaVuSansMono-Oblique.ttf │ └── DejaVuSansMono.ttf ├── resources └── neovim.ico ├── runtime └── plugin │ └── nvim_gui_shim.vim ├── rustfmt.toml ├── screenshots └── neovimgtk-screen.png ├── src ├── cmd_line │ ├── mod.rs │ └── viewport.rs ├── color.rs ├── cursor.rs ├── dirs.rs ├── error.rs ├── file_browser │ ├── mod.rs │ └── tree_view.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 ├── nvim_config.rs ├── nvim_viewport.rs ├── plug_manager │ ├── manager.rs │ ├── mod.rs │ ├── plugin_settings_dlg.rs │ ├── store.rs │ ├── ui.rs │ ├── vim_plug.rs │ └── vimawesome.rs ├── popup_menu │ ├── list_row.rs │ ├── mod.rs │ ├── popover.rs │ └── popupmenu_model.rs ├── project.rs ├── render │ ├── context.rs │ ├── itemize.rs │ └── mod.rs ├── settings.rs ├── shell.rs ├── shell_dlg.rs ├── style.css ├── subscriptions.rs ├── tabline.rs ├── ui.rs ├── ui_model │ ├── cell.rs │ ├── item.rs │ ├── line.rs │ ├── mod.rs │ ├── model_layout.rs │ └── model_rect.rs └── value.rs └── tests ├── cli_tests.rs ├── cmd └── arg_parsing.trycmd └── cmd_unix └── unix_pipes.trycmd /.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/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main", "*stable", "wip/*" ] 6 | pull_request: 7 | branches: [ "main", "*stable", "wip/*" ] 8 | workflow_dispatch: 9 | branches: [ "main", "*stable", "wip/*" ] 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | RUST_BACKTRACE: 1 14 | NVIM_GTK_LOG_FILE: nvim-gtk-stdout.log 15 | NVIM_GTK_LOG_LEVEL: debug 16 | NVIM_GTK_STDERR: nvim-gtk-stderr.log 17 | # Brew update breaks otherwise 18 | HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1 19 | RUSTFLAGS: -C opt-level=0 -D warnings 20 | 21 | jobs: 22 | fedora: 23 | runs-on: [ubuntu-latest] 24 | container: 25 | image: fedora:40 26 | 27 | strategy: 28 | fail-fast: false 29 | matrix: 30 | os: [ubuntu-latest] 31 | include: 32 | - os: ubuntu-latest 33 | 34 | steps: 35 | - uses: actions/checkout@v3 36 | 37 | - uses: Lyude/setup-dnf@main 38 | with: 39 | update: true 40 | install: >- 41 | rust 42 | cargo 43 | rustfmt 44 | clippy 45 | atk-devel 46 | glib2-devel 47 | pango-devel 48 | gtk4-devel 49 | 50 | - uses: Swatinem/rust-cache@v2 51 | 52 | - name: Run cargo test 53 | run: cargo test --locked 54 | 55 | - name: Check that rust-fmt is happy 56 | run: cargo fmt --check -v 57 | 58 | - name: Check for clippy lints 59 | run: cargo clippy -- -Dwarnings 60 | 61 | osx: 62 | runs-on: [macos-latest] 63 | 64 | strategy: 65 | fail-fast: false 66 | matrix: 67 | os: [macos-latest] 68 | rust: [stable] 69 | include: 70 | - os: macos-latest 71 | 72 | steps: 73 | - uses: actions/checkout@v3 74 | 75 | - name: Update brew 76 | run: brew update 77 | 78 | - name: Install dependencies 79 | run: brew install gtk4 pkg-config 80 | 81 | - uses: Swatinem/rust-cache@v2 82 | 83 | - name: Run cargo test 84 | run: cargo test --locked 85 | 86 | windows: 87 | runs-on: [windows-latest] 88 | 89 | strategy: 90 | fail-fast: false 91 | matrix: 92 | os: [windows-latest] 93 | include: 94 | - os: windows-latest 95 | defaults: 96 | run: 97 | shell: msys2 {0} 98 | 99 | steps: 100 | - uses: actions/checkout@v3 101 | 102 | - uses: msys2/setup-msys2@v2 103 | with: 104 | update: true 105 | install: >- 106 | mingw-w64-x86_64-gtk4 107 | mingw-w64-x86_64-binutils 108 | mingw-w64-x86_64-gcc 109 | mingw-w64-x86_64-rust 110 | mingw-w64-x86_64-atk 111 | mingw-w64-x86_64-pango 112 | mingw-w64-x86_64-glib2 113 | mingw-w64-x86_64-pkg-config 114 | 115 | - uses: Swatinem/rust-cache@v2 116 | 117 | - name: Run cargo test 118 | run: cargo test --locked 119 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.swp 3 | 4 | # Some build output from the CI's container caching I believe… 5 | container-images.txt 6 | container-images-key.txt 7 | container-images-uris.txt 8 | container-images.txt 9 | cached-container-images.tar 10 | 11 | # Flatpak 12 | .flatpak-builder 13 | 14 | # vim: tw=100 colorcolumn=100 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v1.0.3 2 | 3 | ## Bugs fixed: 4 | 5 | * Cargo.lock was out of sync, oops 😳. Will look into setting up a release workflow to prevent this 6 | from happening in the future. (fixes #58) 7 | 8 | --- 9 | 10 | # v1.0.2 11 | 12 | New stable release, bugs fixed: 13 | 14 | * OSX (thanks @jacobmischka) 15 | * #46 - Workaround transparent windows bug in GTK4 16 | * Disable default shortcuts 17 | * All 18 | * Fix appearance of error window that displays when initialization fails, which regressed since 19 | GTK4 20 | 21 | ## Additional thanks to 22 | 23 | * @jacobmischka 24 | 25 | --- 26 | 27 | # v1.0.1 28 | 29 | * Update dependencies 30 | * Drop some leftover dead cairo code I missed, and our explicit cairo dependency 31 | * Some bugfixes: 32 | * Fix funny window sizing issue with long file names 33 | (https://github.com/Lyude/neovim-gtk/issues/41) 34 | * Fix scrolling speed on devices like touchpads (https://github.com/Lyude/neovim-gtk/pull/40) 35 | * Aesthetic improvements to the new underline style (underline should no longer look like it's 36 | moving under the text cursor) 37 | * Fix issue with underlines disappearing under the text cursor if no pango item was below the 38 | cursor 39 | 40 | ## Additional thanks to 41 | 42 | * @jadahl 43 | 44 | --- 45 | 46 | # v1.0.0 47 | 48 | We're finally ready to move to GTK4! There's a number of other nice changes that come with this: 49 | 50 | - We now use a `gtk::Snapshot` based renderer instead of cairo, and introduce a new `NvimViewport` 51 | widget that implements the new renderer 52 | - We also convert the `ext_cmdline` over to using the new renderer 53 | - The `ext_popupmenu` popover now uses a `GtkListView` instead of a `GtkTreeView` 54 | - We actually make use of nvim's `flush` event now for screen redraws, which probably should have 55 | been done from the start. Supporting this means we're dramatically less likely to display screen 56 | updates to the user before we've finished parsing a full batch of GUI events from nvim 57 | - We also use the `flush` event for popup menu updates, in addition to flattening all of the which 58 | replaces the previous hacks that were in place to prevent the user from seeing intermediate 59 | `popup_menu` events. This also allows us to avoid having to use a timed delay for displaying the 60 | popup menu, which makes things a bit faster :) 61 | - Long taps from touchscreens should register as right clicks now 62 | 63 | ## Additional thanks to 64 | 65 | - @baco 66 | - @jadahl 67 | - Company and the other folks in `#gtk` who helped a ton with answering questions 68 | 69 | --- 70 | 71 | # v0.4.1 72 | 73 | ## Bug fixes 74 | 75 | - Revert default background type back to dark (#21) 76 | 77 | --- 78 | 79 | # v0.4.0 80 | 81 | Note: this is the first version being maintained by Lyude, and as a result I didn't make a thorough 82 | attempt at coming up with a changelog for history that came before me maintaining things (besides 83 | things that were already written in the changelog by @daa84). Therefore, this changelog may be 84 | incomplete. I've also decided to skip v0.3.0 and go directly to v0.4.0, to indicate the difference 85 | in maintenance since things were stuck on v0.3.0 for so long. Future version bumps won't skip 86 | numbers like this. 87 | 88 | ## Enhancements 89 | 90 | - Migration to new ext_linegrid api [ui-linegrid](https://neovim.io/doc/user/ui.html#ui-linegrid) 91 | - New option --cterm-colors [#190](https://github.com/daa84/neovim-gtk/issues/190) 92 | - Migrate to using nvim-rs instead of neovim-lib, this allows us to use async code and handle 93 | blocking operations far more easily. 94 | - Resize requests are sent immediately vs. intervallically, resulting in much smoother resizing 95 | - We now print RPC request errors as normal neovim error messages 96 | - Closing neovim-gtk is now inhibited during a blocking prompt 97 | - UI elements are now disabled when opening files via the command line, through one of the GUI 98 | elements, or while neovim-gtk is initializing. This prevents potential RPC request timeouts. 99 | - Don't change nvim directory when changing file browser directory, this behavior wasn't immediately 100 | obvious and was more confusing then useful. 101 | - Added support for standout highlighting 102 | - Started populating most of the client info for neovim 103 | - Implemented working maps of some neovim arguments which typically hang the GUI if passed directly 104 | to neovim via `neovim-gtk -- --foo=bar`, including: 105 | - `-c` (execute command after opening files) 106 | - `-d` (diff-mode) 107 | - Start using `nvim_input_mouse()` 108 | - Update GTK+ version to 3.26 109 | - Update crates 110 | - Preliminary work for [GTK+4 support](#8): 111 | - Use `PopoverMenu`s instead of `GtkMenu`s 112 | - Start using `PopoverMenu` and `Action`s for the file browser 113 | - Use `Action`s for building the context menu for the drawing area 114 | - Use a `MenuButton` for the Open button rather than a `Button` 115 | - Use CSS margins instead of `border_width()` where possible 116 | - Stop using `size_allocate` events where we can help it 117 | - Various misc. refactoring 118 | - Use the special color for rendering underline 119 | - Add support for the `:cq` command (#15, @bk2204) 120 | - Improve algorithm for determining popup menu column sizes 121 | - Update GTK+ tabling visibility on tabline option changes 122 | - Make info in the completion popup scrollable 123 | 124 | ## Bug fixes 125 | 126 | - `VimLeavePre` and `VimLeave` are now correctly executed upon exiting 127 | - `E365 ("File already opened in another process")` prompts no longer hang when opening files via 128 | the command line 129 | - The runtime path for our various vim scripts is now correctly set when using `cargo run` 130 | - Resizing while neovim is stuck on a blocking prompt no longer hangs 131 | - Focus changes while neovim is stuck on a blocking prompt no longer hang 132 | - Use the special color for rendering underlines and undercurls when it's explicitly set, otherwise 133 | fallback to the foreground color (except for undercurls, where we default to red in this case). 134 | (#10) 135 | - Fix issues with various unicode graphemes being misaligned when rendered using certain fonts (#7, 136 | #5, @medicalwei) 137 | - Fix crashes and most rendering issues when rendering combining glyphs that require a fallback font 138 | to be used 139 | - Round up background cell width (#1, @jacobmischka) 140 | - Silently ignore the blend attribute for highlights, at least until we've added support for it 141 | (#17, @bk2204). 142 | - Don't use predictably named temporary files (#20, @bk2204) 143 | - Fix undercurl rendering with certain fonts (#11) 144 | - Stop completion popups from changing colors changing when they shouldn't be 145 | - Fix GTK+ tabline visibility issues when trying to disable the external tabline 146 | - Fix undercurl rendering for double width graphemes under the cursor 147 | - Fix coloring with respect to the `background` option in neovim when either one or both of the 148 | foreground and background colors are missing. 149 | 150 | ## Special thanks to those who contributed patches this release 151 | 152 | - @medicalwei 153 | - @bk2204 154 | - @jacobmischka 155 | 156 | 158 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This is a short write-up on the preferred contribution style of this project! Being a rather small 2 | project, we have pretty simple guidelines. Before we go into that though, we must first emphasize 3 | one thing: 4 | 5 | **Don't be afraid!** 6 | 7 | Being a new contributor to a project can be intimidating, and you may feel like you're "not ready 8 | yet" to contribute. But that's OK, the only way to know the answer to that is to give it your best 9 | shot. It's nearly always more valuable from a maintainer's perspective to teach someone how to 10 | contribute to your project then it is to turn them down. 11 | 12 | Now onto the style guidelines: 13 | 14 | * Listen to rustfmt mostly, with the following exceptions: 15 | * Sometimes, particularly when dealing with lots of rendering coordinates, it may be beneficial to 16 | slightly deviate from rustfmt and instead format things by hand. An example of this can be found 17 | in src/cursor.rs: 18 | 19 | ```rust 20 | // … 21 | if state.anim_phase == AnimPhase::NoFocus { 22 | #[rustfmt::skip] 23 | { 24 | let bg = hl.cursor_bg().to_rgbo(filled_alpha); 25 | snapshot.append_color(&bg, &Rect::new( x, y, w, 1.0)); 26 | snapshot.append_color(&bg, &Rect::new( x, y, 1.0, h)); 27 | snapshot.append_color(&bg, &Rect::new( x, y + h - 1.0, w, 1.0)); 28 | snapshot.append_color(&bg, &Rect::new(x + w - 1.0, y, 1.0, h)); 29 | }; 30 | false 31 | } else { 32 | // … 33 | ``` 34 | 35 | There's a lot of variables and arithematic happening here, so it's much easier to read if we 36 | prevent rustfmt from mangling things. Large tables of static data very often fall into this 37 | category. 38 | * `gtk::*Builder` usage patterns. We use these all over the place, and the majority of the time 39 | rustfmt is going to split the method chains here onto separate lines. As a result, single-line 40 | invocations of Builder types tend to look out of place and are less visually intuitive. So, feel 41 | free to have rustfmt skip these whenever it tries combining these method chains onto a single 42 | line. If it's the only Builder invocation in it's scope and it's really, seriously short though, 43 | feel free to use your best judgement. 44 | 45 | * They're guidelines: use your best judgement and don't be afraid of making the wrong decision, if a 46 | piece of code seems like it'd be much more legible without rustfmt mangling it - feel free to 47 | throw a `[rustfmt::skip]` onto it. If a maintainer disagrees, they'll just let you know and the 48 | worst thing you'll have to do is change it ♥. 49 | 50 | vim: tw=100 ts=2 sts=2 sw=2 expandtab 51 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nvim-gtk" 3 | version = "1.1.0-devel" 4 | authors = [ 5 | "daa84 ", 6 | "Lyude Paul ", 7 | ] 8 | build = "build.rs" 9 | edition = "2018" 10 | license = "GPLv3" 11 | 12 | [features] 13 | default = [] 14 | flatpak = [] 15 | 16 | [dependencies] 17 | clap = { version = "4.0", features = ["color", "help", "std", "usage", "error-context", "suggestions", "wrap_help", "derive"] } 18 | glib = "0.20.0" 19 | gio = "0.20.0" 20 | async-trait = "0.1.0" 21 | futures = { version = "0.3.0", features = ["io-compat", "thread-pool"] } 22 | tokio = { version = "1.0", features = ["full"] } 23 | tokio-util = { version = "0.7.0", features = ["full"] } 24 | nvim-rs = { version = "0.9.0", features = ["use_tokio"] } 25 | phf = "0.11.0" 26 | log = "0.4.0" 27 | env_logger = "0.11.0" 28 | html-escape = "0.2.0" 29 | rmpv = { version = "1.0", features = ["with-serde"] } 30 | percent-encoding = "2.0" 31 | regex = "1.0" 32 | unicode-width = "0.2" 33 | unicode-segmentation = "1.0" 34 | fnv = "1.0" 35 | once_cell = "1.0" 36 | 37 | serde = { version = "1.0", features = ["derive"] } 38 | toml = "0.8.0" 39 | serde_json = "1.0" 40 | 41 | is-terminal = "0.4.0" 42 | 43 | [target.'cfg(unix)'.dependencies] 44 | fork = "0.2.0" 45 | 46 | [target.'cfg(windows)'.build-dependencies] 47 | winres = "0.1.0" 48 | 49 | [build-dependencies] 50 | phf_codegen = "0.11.0" 51 | build-version = "0.1.0" 52 | 53 | [dev-dependencies] 54 | trycmd = "0.15.0" 55 | 56 | [dependencies.pango] 57 | features = ["v1_46"] 58 | version = "0.20.0" 59 | 60 | [dependencies.gtk] 61 | package = "gtk4" 62 | version = "0.9.0" 63 | features = ["v4_4"] 64 | 65 | [dependencies.gdk] 66 | package = "gdk4" 67 | version = "0.9.0" 68 | features = ["v4_4"] 69 | 70 | [dependencies.gsk] 71 | package = "gsk4" 72 | version = "0.9.0" 73 | features = ["v4_4"] 74 | -------------------------------------------------------------------------------- /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 $(CARGO_ARGS) -- --no-fork 8 | 9 | install: install-resources 10 | cargo install $(CARGO_ARGS) --path . --force --root $(DESTDIR)$(PREFIX) 11 | 12 | install-flatpak: install 13 | mkdir -p /app/share/metainfo/ 14 | cp desktop/com.github.Lyude.neovim-gtk.metainfo.xml /app/share/metainfo/ 15 | 16 | install-debug: install-resources 17 | cargo install $(CARGO_ARGS) --debug --path . --force --root $(DESTDIR)$(PREFIX) 18 | 19 | install-resources: 20 | mkdir -p $(DESTDIR)$(PREFIX)/share/nvim-gtk/ 21 | cp -r runtime $(DESTDIR)$(PREFIX)/share/nvim-gtk/ 22 | mkdir -p $(DESTDIR)$(PREFIX)/share/applications/ 23 | sed -e "s|Exec=nvim-gtk|Exec=$(PREFIX)/bin/nvim-gtk|" \ 24 | desktop/com.github.Lyude.neovim-gtk.desktop \ 25 | >$(DESTDIR)$(PREFIX)/share/applications/com.github.Lyude.neovim-gtk.desktop 26 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/128x128/apps/ 27 | cp desktop/com.github.Lyude.neovim-gtk_128.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/128x128/apps/com.github.Lyude.neovim-gtk.png 28 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/ 29 | cp desktop/com.github.Lyude.neovim-gtk_48.png $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/com.github.Lyude.neovim-gtk.png 30 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/ 31 | cp desktop/com.github.Lyude.neovim-gtk.svg $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/ 32 | mkdir -p $(DESTDIR)$(PREFIX)/share/icons/hicolor/symbolic/apps/ 33 | cp desktop/com.github.Lyude.neovim-gtk-symbolic.svg $(DESTDIR)$(PREFIX)/share/icons/hicolor/symbolic/apps/ 34 | 35 | uninstall: 36 | rm $(DESTDIR)$(PREFIX)/bin/nvim-gtk 37 | rm -r $(DESTDIR)$(PREFIX)/share/nvim-gtk/ 38 | rm $(DESTDIR)$(PREFIX)/share/applications/com.github.Lyude.neovim-gtk.desktop 39 | rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/128x128/apps/com.github.Lyude.neovim-gtk.png 40 | rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/48x48/apps/com.github.Lyude.neovim-gtk.png 41 | rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/desktop/com.github.Lyude.neovim-gtk.svg 42 | rm $(DESTDIR)$(PREFIX)/share/icons/hicolor/symbolic/apps/desktop/com.github.Lyude.neovim-gtk-symbolic.svg 43 | 44 | .PHONY: all clean test uninstall 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # neovim-gtk 2 | 3 | [![CI](https://github.com/Lyude/neovim-gtk/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Lyude/neovim-gtk/actions/workflows/ci.yml) 4 | 5 | GTK ui for neovim written in rust using gtk-rs bindings. With 6 | [ligatures](https://github.com/daa84/neovim-gtk/wiki/Configuration#ligatures) support. This project 7 | began as a fork of @daa84's neovim-gtk. 8 | 9 | There are a very large number of improvements from @daa84's version, including: 10 | 11 | * Lots of bugfixes 12 | * We're fully ported to GTK4, and have a Snapshot based renderer instead of a cairo based renderer 13 | * _Smooth_ resizing 14 | 15 | Note that I haven't set up the wiki pages for this repo yet, so wiki links still go to daa84's wiki 16 | repo. 17 | 18 | # Screenshot 19 | ![Main Window](/screenshots/neovimgtk-screen.png?raw=true) 20 | 21 | For more screenshots and description of basic usage see [wiki](https://github.com/daa84/neovim-gtk/wiki/GUI) 22 | 23 | # Configuration 24 | To setup font add next line to `ginit.vim` 25 | ```vim 26 | call rpcnotify(1, 'Gui', 'Font', 'DejaVu Sans Mono 12') 27 | ``` 28 | for more details see [wiki](https://github.com/daa84/neovim-gtk/wiki/Configuration) 29 | 30 | # Install 31 | ## From sources 32 | First check [build prerequisites](#build-prerequisites) 33 | 34 | By default to `/usr/local`: 35 | ``` 36 | make install 37 | ``` 38 | Or to some custom path: 39 | ``` 40 | make PREFIX=/some/custom/path install 41 | ``` 42 | 43 | ## Fedora 44 | TODO 45 | ## Arch Linux 46 | TODO 47 | ## openSUSE 48 | TODO 49 | ## Windows 50 | TODO 51 | 52 | # Build prerequisites 53 | ## Linux 54 | First install the GTK development packages. On Debian/Ubuntu derivatives 55 | this can be done as follows: 56 | ``` shell 57 | apt install libgtk-4-dev 58 | ``` 59 | 60 | On Fedora: 61 | ```bash 62 | dnf install atk-devel glib2-devel pango-devel gtk4-devel 63 | ``` 64 | 65 | Then install the latest rust compiler, best with the 66 | [rustup tool](https://rustup.rs/). The build command: 67 | ``` 68 | cargo build --release 69 | ``` 70 | 71 | As of writing this (Dec 16, 2022) the packaged rust tools in Fedora also work for building. 72 | 73 | ## Windows 74 | Neovim-gtk can be compiled using MSYS2 GTK packages. In this case use 'windows-gnu' rust toolchain. 75 | ``` 76 | SET PKG_CONFIG_PATH=C:\msys64\mingw64\lib\pkgconfig 77 | cargo build --release 78 | ``` 79 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs::File; 3 | use std::io::{BufWriter, Write}; 4 | use std::path::{Path, PathBuf}; 5 | use std::process::Command; 6 | 7 | fn main() { 8 | let out_dir = &env::var("OUT_DIR").unwrap(); 9 | 10 | build_version::write_version_file().expect("Failed to write version.rs file"); 11 | 12 | if cfg!(target_os = "windows") { 13 | println!("cargo:rustc-link-search=native=C:\\msys64\\mingw64\\lib"); 14 | 15 | set_win_icon(); 16 | } 17 | 18 | let path = Path::new(out_dir).join("key_map_table.rs"); 19 | let mut file = BufWriter::new(File::create(path).unwrap()); 20 | 21 | writeln!( 22 | &mut file, 23 | "static KEYVAL_MAP: phf::Map<&'static str, &'static str> = \n{};\n", 24 | phf_codegen::Map::new() 25 | .entry("F1", "\"F1\"") 26 | .entry("F2", "\"F2\"") 27 | .entry("F3", "\"F3\"") 28 | .entry("F4", "\"F4\"") 29 | .entry("F5", "\"F5\"") 30 | .entry("F6", "\"F6\"") 31 | .entry("F7", "\"F7\"") 32 | .entry("F8", "\"F8\"") 33 | .entry("F9", "\"F9\"") 34 | .entry("F10", "\"F10\"") 35 | .entry("F11", "\"F11\"") 36 | .entry("F12", "\"F12\"") 37 | .entry("Left", "\"Left\"") 38 | .entry("Right", "\"Right\"") 39 | .entry("Up", "\"Up\"") 40 | .entry("Down", "\"Down\"") 41 | .entry("Home", "\"Home\"") 42 | .entry("End", "\"End\"") 43 | .entry("BackSpace", "\"BS\"") 44 | .entry("Return", "\"CR\"") 45 | .entry("Escape", "\"Esc\"") 46 | .entry("Delete", "\"Del\"") 47 | .entry("Insert", "\"Insert\"") 48 | .entry("Page_Up", "\"PageUp\"") 49 | .entry("Page_Down", "\"PageDown\"") 50 | .entry("Enter", "\"CR\"") 51 | .entry("Tab", "\"Tab\"") 52 | .entry("ISO_Left_Tab", "\"Tab\"") 53 | .build() 54 | ) 55 | .unwrap(); 56 | 57 | println!( 58 | "cargo:rustc-env=RUNTIME_PATH={}", 59 | if let Some(prefix) = option_env!("PREFIX") { 60 | PathBuf::from(prefix).join("share/nvim-gtk/runtime") 61 | } else { 62 | PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("runtime") 63 | } 64 | .to_str() 65 | .unwrap() 66 | ); 67 | 68 | if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() { 69 | println!( 70 | "cargo:rustc-env=GIT_COMMIT={}", 71 | String::from_utf8(output.stdout).unwrap() 72 | ); 73 | } 74 | } 75 | 76 | #[cfg(windows)] 77 | fn set_win_icon() { 78 | let mut res = winres::WindowsResource::new(); 79 | res.set_icon("resources/neovim.ico"); 80 | if let Err(err) = res.compile() { 81 | eprintln!("Error set icon: {}", err); 82 | } 83 | } 84 | 85 | #[cfg(unix)] 86 | fn set_win_icon() {} 87 | -------------------------------------------------------------------------------- /com.github.Lyude.neovim-gtk.yaml: -------------------------------------------------------------------------------- 1 | app-id: com.github.Lyude.neovim-gtk 2 | runtime: org.gnome.Platform 3 | runtime-version: '45' 4 | sdk: org.gnome.Sdk 5 | sdk-extensions: 6 | - org.freedesktop.Sdk.Extension.rust-stable 7 | command: nvim-gtk 8 | finish-args: 9 | - --share=ipc 10 | - --socket=fallback-x11 11 | - --socket=wayland 12 | - --device=dri 13 | - --socket=session-bus # for `flatpak-spawn --host nvim` 14 | build-options: 15 | append-path: "/usr/lib/sdk/rust-stable/bin" 16 | build-args: 17 | - "--share=network" # for cargo fetching dependencies 18 | env: 19 | CARGO_HOME: "/run/build/neovim-gtk" # for caching 20 | CARGO_ARGS: "--features flatpak" 21 | PREFIX: "/app" 22 | modules: 23 | - name: neovim-gtk 24 | buildsystem: simple 25 | build-commands: 26 | - make install-flatpak 27 | sources: 28 | - type: archive 29 | #url: https://github.com/Lyude/neovim-gtk/archive/refs/tags/v1.0.4.tar.gz 30 | #sha256: d0d0dacfbfca16168361f517dee20259785379910173cc33d7d48bd301d30f18 31 | url: https://github.com/Lyude/neovim-gtk/archive/3739f961d28d2a7c98b1fd8be912fc4bb9d9d216.tar.gz 32 | sha256: 78f0a12bdbf5d085fdbc0a57d877b695c5ae4873d036dd5e190141a927da2819 33 | -------------------------------------------------------------------------------- /desktop/com.github.Lyude.neovim-gtk-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/com.github.Lyude.neovim-gtk.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=NeovimGtk 3 | Comment=Gtk GUI for Neovim text editor 4 | Exec=nvim-gtk -- %F 5 | Icon=com.github.Lyude.neovim-gtk 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/com.github.Lyude.neovim-gtk.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.github.Lyude.neovim-gtk 5 | neovim-gtk 6 | GTK UI for neovim written in rust using gtk-rs bindings 7 | Lyude Paul, @daa84 8 | 9 |

GTK ui for neovim written in rust using gtk-rs bindings. With ligatures support. This project began as a fork of @daa84's neovim-gtk.

10 |

There are a very large number of improvements from @daa84's version, including: 11 |

    12 |
  • Lots of bugfixes
  • 13 |
  • We're fully ported to GTK4, and have a Snapshot based renderer instead of a cairo based renderer
  • 14 |
  • Smooth resizing
  • 15 |
16 |

17 |
18 | GPL-3.0 19 | 20 | neovim 21 | vim 22 | editor 23 | 24 | 25 | network 26 | web 27 | 28 | https://github.com/Lyude/neovim-gtk 29 | https://github.com/Lyude/neovim-gtk/issues/new 30 | 31 | 32 | https://raw.githubusercontent.com/Lyude/neovim-gtk/main/screenshots/neovimgtk-screen.png 33 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /desktop/com.github.Lyude.neovim-gtk.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/com.github.Lyude.neovim-gtk_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/com.github.Lyude.neovim-gtk_128.png -------------------------------------------------------------------------------- /desktop/com.github.Lyude.neovim-gtk_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/com.github.Lyude.neovim-gtk_48.png -------------------------------------------------------------------------------- /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/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/dejavu_font/DejaVuSansMono-Bold.ttf -------------------------------------------------------------------------------- /desktop/dejavu_font/DejaVuSansMono-BoldOblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/dejavu_font/DejaVuSansMono-BoldOblique.ttf -------------------------------------------------------------------------------- /desktop/dejavu_font/DejaVuSansMono-Oblique.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/dejavu_font/DejaVuSansMono-Oblique.ttf -------------------------------------------------------------------------------- /desktop/dejavu_font/DejaVuSansMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/desktop/dejavu_font/DejaVuSansMono.ttf -------------------------------------------------------------------------------- /resources/neovim.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/resources/neovim.ico -------------------------------------------------------------------------------- /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 g:clipboard = { 9 | \ 'name': 'neovim-gtk', 10 | \ 'copy': { 11 | \ '+': { lines, regtype -> rpcnotify(1, 'Gui', 'Clipboard', 'Set', regtype, join(lines, ' ')) }, 12 | \ '*': { lines, regtype -> rpcnotify(1, 'Gui', 'Clipboard', 'Set', regtype, join(lines, ' ')) }, 13 | \ }, 14 | \ 'paste': { 15 | \ '+': { -> rpcrequest(1, 'Gui', 'Clipboard', 'Get', '+') }, 16 | \ '*': { -> rpcrequest(1, 'Gui', 'Clipboard', 'Get', '*') }, 17 | \ }, 18 | \ } 19 | endif 20 | 21 | " Set GUI font 22 | function! GuiFont(fname, ...) abort 23 | call rpcnotify(1, 'Gui', 'Font', s:NvimQtToPangoFont(a:fname)) 24 | endfunction 25 | 26 | " Some subset of parse command from neovim-qt 27 | " to support interoperability 28 | function s:NvimQtToPangoFont(fname) 29 | let l:attrs = split(a:fname, ':') 30 | let l:size = -1 31 | for part in l:attrs 32 | if len(part) >= 2 && part[0] == 'h' 33 | let l:size = strpart(part, 1) 34 | endif 35 | endfor 36 | 37 | if l:size > 0 38 | return l:attrs[0] . ' ' . l:size 39 | endif 40 | 41 | return l:attrs[0] 42 | endf 43 | 44 | 45 | " The GuiFont command. For compatibility there is also Guifont 46 | function s:GuiFontCommand(fname, bang) abort 47 | if a:fname ==# '' 48 | if exists('g:GuiFont') 49 | echo g:GuiFont 50 | else 51 | echo 'No GuiFont is set' 52 | endif 53 | else 54 | call GuiFont(a:fname, a:bang ==# '!') 55 | endif 56 | endfunction 57 | command! -nargs=1 -bang Guifont call s:GuiFontCommand("", "") 58 | command! -nargs=1 -bang GuiFont call s:GuiFontCommand("", "") 59 | 60 | command! -nargs=? GuiFontFeatures call rpcnotify(1, 'Gui', 'FontFeatures', ) 61 | command! -nargs=1 GuiLinespace call rpcnotify(1, 'Gui', 'Linespace', ) 62 | 63 | command! NGToggleSidebar call rpcnotify(1, 'Gui', 'Command', 'ToggleSidebar') 64 | command! NGShowProjectView call rpcnotify(1, 'Gui', 'Command', 'ShowProjectView') 65 | command! -nargs=+ NGTransparency call rpcnotify(1, 'Gui', 'Command', 'Transparency', ) 66 | command! -nargs=1 NGPreferDarkTheme call rpcnotify(1, 'Gui', 'Command', 'PreferDarkTheme', ) 67 | command! -nargs=1 NGSetCursorBlink call rpcnotify(1, 'Gui', 'Command', 'SetCursorBlink', ) 68 | 69 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2018" 2 | 3 | use_try_shorthand = true 4 | use_field_init_shorthand = true 5 | -------------------------------------------------------------------------------- /screenshots/neovimgtk-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyude/neovim-gtk/f36cfeca7d0a678f6eb50342739aca5258077126/screenshots/neovimgtk-screen.png -------------------------------------------------------------------------------- /src/cmd_line/viewport.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | 3 | use gtk::{ 4 | graphene::{Point, Rect}, 5 | prelude::*, 6 | subclass::prelude::*, 7 | }; 8 | 9 | use std::{ 10 | cell::RefCell, 11 | sync::{Arc, Weak}, 12 | }; 13 | 14 | use crate::{render::*, shell::TransparencySettings, ui::UiMutex}; 15 | 16 | use crate::cmd_line::State; 17 | 18 | glib::wrapper! { 19 | pub struct CmdlineViewport(ObjectSubclass) 20 | @extends gtk::Widget, 21 | @implements gtk::Accessible; 22 | } 23 | 24 | impl CmdlineViewport { 25 | pub fn new() -> Self { 26 | glib::Object::new::() 27 | } 28 | 29 | pub fn set_state(&self, state: &Arc>) { 30 | self.set_property("cmdline-state", glib::BoxedAnyObject::new(state.clone())); 31 | } 32 | 33 | pub fn clear_snapshot_cache(&self) { 34 | self.set_property("snapshot-cached", false); 35 | } 36 | } 37 | 38 | #[derive(Default)] 39 | struct CmdlineViewportInner { 40 | state: Weak>, 41 | block_cache: Option, 42 | level_cache: Option, 43 | } 44 | 45 | #[derive(Default)] 46 | pub struct CmdlineViewportObject { 47 | inner: RefCell, 48 | } 49 | 50 | #[glib::object_subclass] 51 | impl ObjectSubclass for CmdlineViewportObject { 52 | const NAME: &'static str = "NvimCmdlineViewport"; 53 | type Type = CmdlineViewport; 54 | type ParentType = gtk::Widget; 55 | 56 | fn class_init(klass: &mut Self::Class) { 57 | klass.set_css_name("widget"); 58 | klass.set_accessible_role(gtk::AccessibleRole::TextBox); 59 | } 60 | } 61 | 62 | impl ObjectImpl for CmdlineViewportObject { 63 | fn properties() -> &'static [glib::ParamSpec] { 64 | static PROPERTIES: Lazy> = Lazy::new(|| { 65 | vec![ 66 | glib::ParamSpecObject::builder::("cmdline-state") 67 | .write_only() 68 | .build(), 69 | glib::ParamSpecBoolean::builder("snapshot-cached").build(), 70 | ] 71 | }); 72 | 73 | PROPERTIES.as_ref() 74 | } 75 | 76 | fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 77 | match pspec.name() { 78 | "cmdline-state" => { 79 | let mut inner = self.inner.borrow_mut(); 80 | debug_assert!(inner.state.upgrade().is_none()); 81 | 82 | inner.state = 83 | Arc::downgrade(&value.get::().unwrap().borrow()); 84 | } 85 | "snapshot-cached" => { 86 | if !value.get::().unwrap() { 87 | let mut inner = self.inner.borrow_mut(); 88 | inner.block_cache = None; 89 | inner.level_cache = None; 90 | } 91 | } 92 | _ => unreachable!(), 93 | } 94 | } 95 | 96 | fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { 97 | match pspec.name() { 98 | "snapshot-cached" => { 99 | let inner = self.inner.borrow(); 100 | (inner.level_cache.is_some() || inner.block_cache.is_some()).to_value() 101 | } 102 | _ => unreachable!(), 103 | } 104 | } 105 | } 106 | 107 | impl WidgetImpl for CmdlineViewportObject { 108 | fn snapshot(&self, snapshot: >k::Snapshot) { 109 | let obj = self.obj(); 110 | let mut inner = self.inner.borrow_mut(); 111 | let state = match inner.state.upgrade() { 112 | Some(state) => state, 113 | None => return, 114 | }; 115 | let mut state = state.borrow_mut(); 116 | let render_state = state.render_state.clone(); 117 | let render_state = render_state.borrow(); 118 | let font_ctx = &render_state.font_ctx; 119 | let hl = &render_state.hl; 120 | 121 | snapshot.append_color( 122 | &hl.bg().into(), 123 | &Rect::new(0.0, 0.0, obj.width() as f32, obj.height() as f32), 124 | ); 125 | 126 | snapshot.save(); 127 | 128 | let preferred_height = state.preferred_height(); 129 | let gap = obj.height() - preferred_height; 130 | if gap > 0 { 131 | snapshot.translate(&Point::new(0.0, (gap / 2) as f32)); 132 | } 133 | 134 | if let Some(block) = state.block.as_mut() { 135 | if inner.block_cache.is_none() { 136 | inner.block_cache = snapshot_nvim(font_ctx, &block.model_layout.model, hl); 137 | } 138 | if let Some(ref cache) = inner.block_cache { 139 | snapshot.append_node(cache); 140 | } 141 | 142 | snapshot.translate(&Point::new(0.0, block.preferred_height as f32)); 143 | } 144 | 145 | if let Some(level) = state.levels.last_mut() { 146 | if inner.level_cache.is_none() { 147 | inner.level_cache = snapshot_nvim(font_ctx, &level.model_layout.model, hl); 148 | } 149 | if let Some(ref cache) = inner.level_cache { 150 | snapshot.append_node(cache); 151 | } 152 | } 153 | 154 | if let Some(level) = state.levels.last() { 155 | if let Some(ref cursor) = state.cursor { 156 | snapshot_cursor( 157 | snapshot, 158 | cursor, 159 | font_ctx, 160 | &level.model_layout.model, 161 | hl, 162 | TransparencySettings::new(), // FIXME 163 | ); 164 | } 165 | } 166 | 167 | snapshot.restore(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/color.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy, PartialEq, Debug, Default)] 2 | pub struct Color(pub f64, pub f64, pub f64); 3 | 4 | pub const COLOR_BLACK: Color = Color(0.0, 0.0, 0.0); 5 | pub const COLOR_WHITE: Color = Color(1.0, 1.0, 1.0); 6 | pub const COLOR_RED: Color = Color(1.0, 0.0, 0.0); 7 | 8 | impl From<&Color> for gdk::RGBA { 9 | fn from(color: &Color) -> Self { 10 | color.to_rgbo(1.0) 11 | } 12 | } 13 | 14 | impl Color { 15 | pub fn from_cterm(idx: u8) -> Color { 16 | let color = TERMINAL_COLORS[usize::from(idx)]; 17 | Color( 18 | color.0 as f64 / 255.0, 19 | color.1 as f64 / 255.0, 20 | color.2 as f64 / 255.0, 21 | ) 22 | } 23 | 24 | pub fn from_indexed_color(indexed_color: u64) -> Color { 25 | let r = ((indexed_color >> 16) & 0xff) as f64; 26 | let g = ((indexed_color >> 8) & 0xff) as f64; 27 | let b = (indexed_color & 0xff) as f64; 28 | Color(r / 255.0, g / 255.0, b / 255.0) 29 | } 30 | 31 | pub fn to_u16(self) -> (u16, u16, u16) { 32 | ( 33 | (u16::MAX as f64 * self.0) as u16, 34 | (u16::MAX as f64 * self.1) as u16, 35 | (u16::MAX as f64 * self.2) as u16, 36 | ) 37 | } 38 | 39 | pub fn to_hex(self) -> String { 40 | format!( 41 | "#{:02X}{:02X}{:02X}", 42 | (self.0 * 255.0) as u8, 43 | (self.1 * 255.0) as u8, 44 | (self.2 * 255.0) as u8, 45 | ) 46 | } 47 | 48 | pub fn to_rgbo(self, alpha: f64) -> gdk::RGBA { 49 | gdk::RGBA::new(self.0 as f32, self.1 as f32, self.2 as f32, alpha as f32) 50 | } 51 | 52 | pub fn to_pango_bg(self) -> pango::AttrColor { 53 | let (r, g, b) = self.to_u16(); 54 | 55 | pango::AttrColor::new_background(r, g, b) 56 | } 57 | 58 | pub fn to_pango_fg(self) -> pango::AttrColor { 59 | let (r, g, b) = self.to_u16(); 60 | 61 | pango::AttrColor::new_foreground(r, g, b) 62 | } 63 | 64 | pub fn invert(&self) -> Self { 65 | Self(1.0 - self.0, 1.0 - self.1, 1.0 - self.2) 66 | } 67 | 68 | pub fn fade(&self, into: &Self, percentage: f64) -> Self { 69 | debug_assert!((0.0..=1.0).contains(&percentage)); 70 | 71 | match percentage { 72 | _ if percentage <= 0.000001 => *self, 73 | _ if percentage >= 0.999999 => *into, 74 | _ => { 75 | let inv = (into.0 - self.0, into.1 - self.1, into.2 - self.2); 76 | Self( 77 | self.0 + (inv.0 * percentage), 78 | self.1 + (inv.1 * percentage), 79 | self.2 + (inv.2 * percentage), 80 | ) 81 | } 82 | } 83 | } 84 | } 85 | 86 | /// From https://jonasjacek.github.io/colors/ 87 | const TERMINAL_COLORS: [(u8, u8, u8); 256] = [ 88 | (0, 0, 0), 89 | (128, 0, 0), 90 | (0, 128, 0), 91 | (128, 128, 0), 92 | (0, 0, 128), 93 | (128, 0, 128), 94 | (0, 128, 128), 95 | (192, 192, 192), 96 | (128, 128, 128), 97 | (255, 0, 0), 98 | (0, 255, 0), 99 | (255, 255, 0), 100 | (0, 0, 255), 101 | (255, 0, 255), 102 | (0, 255, 255), 103 | (255, 255, 255), 104 | (0, 0, 0), 105 | (0, 0, 95), 106 | (0, 0, 135), 107 | (0, 0, 175), 108 | (0, 0, 215), 109 | (0, 0, 255), 110 | (0, 95, 0), 111 | (0, 95, 95), 112 | (0, 95, 135), 113 | (0, 95, 175), 114 | (0, 95, 215), 115 | (0, 95, 255), 116 | (0, 135, 0), 117 | (0, 135, 95), 118 | (0, 135, 135), 119 | (0, 135, 175), 120 | (0, 135, 215), 121 | (0, 135, 255), 122 | (0, 175, 0), 123 | (0, 175, 95), 124 | (0, 175, 135), 125 | (0, 175, 175), 126 | (0, 175, 215), 127 | (0, 175, 255), 128 | (0, 215, 0), 129 | (0, 215, 95), 130 | (0, 215, 135), 131 | (0, 215, 175), 132 | (0, 215, 215), 133 | (0, 215, 255), 134 | (0, 255, 0), 135 | (0, 255, 95), 136 | (0, 255, 135), 137 | (0, 255, 175), 138 | (0, 255, 215), 139 | (0, 255, 255), 140 | (95, 0, 0), 141 | (95, 0, 95), 142 | (95, 0, 135), 143 | (95, 0, 175), 144 | (95, 0, 215), 145 | (95, 0, 255), 146 | (95, 95, 0), 147 | (95, 95, 95), 148 | (95, 95, 135), 149 | (95, 95, 175), 150 | (95, 95, 215), 151 | (95, 95, 255), 152 | (95, 135, 0), 153 | (95, 135, 95), 154 | (95, 135, 135), 155 | (95, 135, 175), 156 | (95, 135, 215), 157 | (95, 135, 255), 158 | (95, 175, 0), 159 | (95, 175, 95), 160 | (95, 175, 135), 161 | (95, 175, 175), 162 | (95, 175, 215), 163 | (95, 175, 255), 164 | (95, 215, 0), 165 | (95, 215, 95), 166 | (95, 215, 135), 167 | (95, 215, 175), 168 | (95, 215, 215), 169 | (95, 215, 255), 170 | (95, 255, 0), 171 | (95, 255, 95), 172 | (95, 255, 135), 173 | (95, 255, 175), 174 | (95, 255, 215), 175 | (95, 255, 255), 176 | (135, 0, 0), 177 | (135, 0, 95), 178 | (135, 0, 135), 179 | (135, 0, 175), 180 | (135, 0, 215), 181 | (135, 0, 255), 182 | (135, 95, 0), 183 | (135, 95, 95), 184 | (135, 95, 135), 185 | (135, 95, 175), 186 | (135, 95, 215), 187 | (135, 95, 255), 188 | (135, 135, 0), 189 | (135, 135, 95), 190 | (135, 135, 135), 191 | (135, 135, 175), 192 | (135, 135, 215), 193 | (135, 135, 255), 194 | (135, 175, 0), 195 | (135, 175, 95), 196 | (135, 175, 135), 197 | (135, 175, 175), 198 | (135, 175, 215), 199 | (135, 175, 255), 200 | (135, 215, 0), 201 | (135, 215, 95), 202 | (135, 215, 135), 203 | (135, 215, 175), 204 | (135, 215, 215), 205 | (135, 215, 255), 206 | (135, 255, 0), 207 | (135, 255, 95), 208 | (135, 255, 135), 209 | (135, 255, 175), 210 | (135, 255, 215), 211 | (135, 255, 255), 212 | (175, 0, 0), 213 | (175, 0, 95), 214 | (175, 0, 135), 215 | (175, 0, 175), 216 | (175, 0, 215), 217 | (175, 0, 255), 218 | (175, 95, 0), 219 | (175, 95, 95), 220 | (175, 95, 135), 221 | (175, 95, 175), 222 | (175, 95, 215), 223 | (175, 95, 255), 224 | (175, 135, 0), 225 | (175, 135, 95), 226 | (175, 135, 135), 227 | (175, 135, 175), 228 | (175, 135, 215), 229 | (175, 135, 255), 230 | (175, 175, 0), 231 | (175, 175, 95), 232 | (175, 175, 135), 233 | (175, 175, 175), 234 | (175, 175, 215), 235 | (175, 175, 255), 236 | (175, 215, 0), 237 | (175, 215, 95), 238 | (175, 215, 135), 239 | (175, 215, 175), 240 | (175, 215, 215), 241 | (175, 215, 255), 242 | (175, 255, 0), 243 | (175, 255, 95), 244 | (175, 255, 135), 245 | (175, 255, 175), 246 | (175, 255, 215), 247 | (175, 255, 255), 248 | (215, 0, 0), 249 | (215, 0, 95), 250 | (215, 0, 135), 251 | (215, 0, 175), 252 | (215, 0, 215), 253 | (215, 0, 255), 254 | (215, 95, 0), 255 | (215, 95, 95), 256 | (215, 95, 135), 257 | (215, 95, 175), 258 | (215, 95, 215), 259 | (215, 95, 255), 260 | (215, 135, 0), 261 | (215, 135, 95), 262 | (215, 135, 135), 263 | (215, 135, 175), 264 | (215, 135, 215), 265 | (215, 135, 255), 266 | (215, 175, 0), 267 | (215, 175, 95), 268 | (215, 175, 135), 269 | (215, 175, 175), 270 | (215, 175, 215), 271 | (215, 175, 255), 272 | (215, 215, 0), 273 | (215, 215, 95), 274 | (215, 215, 135), 275 | (215, 215, 175), 276 | (215, 215, 215), 277 | (215, 215, 255), 278 | (215, 255, 0), 279 | (215, 255, 95), 280 | (215, 255, 135), 281 | (215, 255, 175), 282 | (215, 255, 215), 283 | (215, 255, 255), 284 | (255, 0, 0), 285 | (255, 0, 95), 286 | (255, 0, 135), 287 | (255, 0, 175), 288 | (255, 0, 215), 289 | (255, 0, 255), 290 | (255, 95, 0), 291 | (255, 95, 95), 292 | (255, 95, 135), 293 | (255, 95, 175), 294 | (255, 95, 215), 295 | (255, 95, 255), 296 | (255, 135, 0), 297 | (255, 135, 95), 298 | (255, 135, 135), 299 | (255, 135, 175), 300 | (255, 135, 215), 301 | (255, 135, 255), 302 | (255, 175, 0), 303 | (255, 175, 95), 304 | (255, 175, 135), 305 | (255, 175, 175), 306 | (255, 175, 215), 307 | (255, 175, 255), 308 | (255, 215, 0), 309 | (255, 215, 95), 310 | (255, 215, 135), 311 | (255, 215, 175), 312 | (255, 215, 215), 313 | (255, 215, 255), 314 | (255, 255, 0), 315 | (255, 255, 95), 316 | (255, 255, 135), 317 | (255, 255, 175), 318 | (255, 255, 215), 319 | (255, 255, 255), 320 | (8, 8, 8), 321 | (18, 18, 18), 322 | (28, 28, 28), 323 | (38, 38, 38), 324 | (48, 48, 48), 325 | (58, 58, 58), 326 | (68, 68, 68), 327 | (78, 78, 78), 328 | (88, 88, 88), 329 | (98, 98, 98), 330 | (108, 108, 108), 331 | (118, 118, 118), 332 | (128, 128, 128), 333 | (138, 138, 138), 334 | (148, 148, 148), 335 | (158, 158, 158), 336 | (168, 168, 168), 337 | (178, 178, 178), 338 | (188, 188, 188), 339 | (198, 198, 198), 340 | (208, 208, 208), 341 | (218, 218, 218), 342 | (228, 228, 228), 343 | (238, 238, 238), 344 | ]; 345 | 346 | #[cfg(test)] 347 | mod tests { 348 | use super::*; 349 | 350 | #[test] 351 | fn test_to_hex() { 352 | let col = Color(0.0, 1.0, 0.0); 353 | assert_eq!("#00FF00", &col.to_hex()); 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /src/dirs.rs: -------------------------------------------------------------------------------- 1 | use std::path::*; 2 | 3 | use once_cell::sync::Lazy; 4 | 5 | pub fn app_config_dir_create() -> Result { 6 | let config_dir = app_config_dir(); 7 | 8 | std::fs::create_dir_all(config_dir).map_err(|e| format!("{e}"))?; 9 | 10 | Ok(config_dir.to_path_buf()) 11 | } 12 | 13 | pub fn app_config_dir() -> &'static Path { 14 | static DIR: Lazy = Lazy::new(|| glib::user_config_dir().join("nvim-gtk")); 15 | DIR.as_path() 16 | } 17 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use log::error; 4 | 5 | use html_escape::encode_text_minimal; 6 | 7 | use gtk::prelude::*; 8 | 9 | use crate::shell; 10 | 11 | pub struct ErrorArea { 12 | base: gtk::Box, 13 | label: gtk::Label, 14 | } 15 | 16 | impl ErrorArea { 17 | pub fn new() -> Self { 18 | let base = gtk::Box::builder() 19 | .orientation(gtk::Orientation::Horizontal) 20 | .spacing(10) 21 | .valign(gtk::Align::Center) 22 | .halign(gtk::Align::Center) 23 | .vexpand(true) 24 | .hexpand(true) 25 | .build(); 26 | 27 | let label = gtk::Label::builder() 28 | .wrap(true) 29 | .selectable(true) 30 | .hexpand(true) 31 | .vexpand(true) 32 | .build(); 33 | 34 | let error_image = gtk::Image::from_icon_name("dialog-error"); 35 | error_image.set_icon_size(gtk::IconSize::Large); 36 | error_image.set_halign(gtk::Align::End); 37 | error_image.set_valign(gtk::Align::Center); 38 | error_image.set_hexpand(true); 39 | error_image.set_vexpand(true); 40 | 41 | base.append(&error_image); 42 | base.append(&label); 43 | 44 | ErrorArea { base, label } 45 | } 46 | 47 | pub fn show_nvim_init_error(&self, err: &str) { 48 | error!("Can't initialize nvim: {}", err); 49 | self.label.set_markup(&format!( 50 | "Can't initialize nvim:\n\ 51 | {}\n\n\ 52 | Possible error reasons:\n\ 53 | ● Not supported nvim version (minimum supported version is {})\n\ 54 | ● Error in configuration file (init.vim or ginit.vim)", 55 | encode_text_minimal(err), 56 | shell::MINIMUM_SUPPORTED_NVIM_VERSION 57 | )); 58 | self.base.show(); 59 | } 60 | 61 | pub fn show_nvim_start_error(&self, err: &str, cmd: &str) { 62 | error!("Can't start nvim: {}\nCommand line: {}", err, cmd); 63 | self.label.set_markup(&format!( 64 | "Can't start nvim instance:\n\ 65 | {}\n\ 66 | {}\n\n\ 67 | Possible error reasons:\n\ 68 | ● Not supported nvim version (minimum supported version is {})\n\ 69 | ● Error in configuration file (init.vim or ginit.vim)\n\ 70 | ● Wrong nvim binary path \ 71 | (right path can be passed with --nvim-bin-path=path_here)", 72 | encode_text_minimal(cmd), 73 | encode_text_minimal(err), 74 | shell::MINIMUM_SUPPORTED_NVIM_VERSION 75 | )); 76 | self.base.show(); 77 | } 78 | 79 | pub fn show_nvim_tcp_connect_error(&self, err: &str, addr: &str) { 80 | error!("Can't connect to nvim on TCP address {}: {}\n", addr, err); 81 | self.label.set_markup(&format!( 82 | "Can't connect to nvim instance on TCP address {}:\n\ 83 | {}\n\ 84 | Possible error reasons:\n\ 85 | ● Not supported nvim version (minimum supported version is {})\n\ 86 | ● Error in configuration file (init.vim or ginit.vim)\n\ 87 | ● Invalid TCP address", 88 | encode_text_minimal(addr), 89 | encode_text_minimal(err), 90 | shell::MINIMUM_SUPPORTED_NVIM_VERSION 91 | )); 92 | self.base.show(); 93 | } 94 | 95 | #[cfg(unix)] 96 | pub fn show_nvim_unix_connect_error(&self, err: &str, addr: &str) { 97 | error!("Can't connect to nvim on Unix pipe {}: {}\n", addr, err); 98 | self.label.set_markup(&format!( 99 | "Can't connect to nvim instance on Unix pipe {}:\n\ 100 | {}\n\ 101 | Possible error reasons:\n\ 102 | ● Not supported nvim version (minimum supported version is {})\n\ 103 | ● Error in configuration file (init.vim or ginit.vim)\n\ 104 | ● Invalid Unix pipe", 105 | encode_text_minimal(addr), 106 | encode_text_minimal(err), 107 | shell::MINIMUM_SUPPORTED_NVIM_VERSION 108 | )); 109 | self.base.show(); 110 | } 111 | } 112 | 113 | impl Deref for ErrorArea { 114 | type Target = gtk::Box; 115 | 116 | fn deref(&self) -> >k::Box { 117 | &self.base 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/file_browser/tree_view.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | 3 | use gtk::{prelude::*, subclass::prelude::*}; 4 | 5 | glib::wrapper! { 6 | pub struct TreeView(ObjectSubclass) 7 | @extends gtk::Widget, gtk::TreeView; 8 | } 9 | 10 | /// A popup-aware TreeView widget for the file browser pane 11 | impl TreeView { 12 | pub fn new() -> Self { 13 | glib::Object::new::() 14 | } 15 | 16 | pub fn set_context_menu(&self, context_menu: >k::PopoverMenu) { 17 | self.set_property("context-menu", context_menu); 18 | } 19 | } 20 | 21 | #[derive(Default)] 22 | pub struct TreeViewObject { 23 | context_menu: glib::WeakRef, 24 | } 25 | 26 | #[glib::object_subclass] 27 | impl ObjectSubclass for TreeViewObject { 28 | const NAME: &'static str = "NvimFileBrowserTreeView"; 29 | type Type = TreeView; 30 | type ParentType = gtk::TreeView; 31 | } 32 | 33 | impl ObjectImpl for TreeViewObject { 34 | fn dispose(&self) { 35 | if let Some(context_menu) = self.context_menu.upgrade() { 36 | context_menu.unparent(); 37 | } 38 | } 39 | 40 | fn properties() -> &'static [glib::ParamSpec] { 41 | static PROPERTIES: Lazy> = Lazy::new(|| { 42 | vec![glib::ParamSpecObject::builder::("context-menu").build()] 43 | }); 44 | 45 | PROPERTIES.as_ref() 46 | } 47 | 48 | fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 49 | let obj = self.obj(); 50 | match pspec.name() { 51 | "context-menu" => { 52 | if let Some(context_menu) = self.context_menu.upgrade() { 53 | context_menu.unparent(); 54 | } 55 | 56 | let context_menu: gtk::PopoverMenu = value.get().unwrap(); 57 | context_menu.set_parent(&*obj); 58 | self.context_menu.set(Some(&context_menu)); 59 | } 60 | _ => unimplemented!(), 61 | } 62 | } 63 | 64 | fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { 65 | match pspec.name() { 66 | "context-menu" => self.context_menu.upgrade().to_value(), 67 | _ => unimplemented!(), 68 | } 69 | } 70 | } 71 | 72 | impl WidgetImpl for TreeViewObject { 73 | fn size_allocate(&self, width: i32, height: i32, baseline: i32) { 74 | self.parent_size_allocate(width, height, baseline); 75 | self.context_menu.upgrade().unwrap().present(); 76 | } 77 | } 78 | 79 | impl gtk::subclass::prelude::TreeViewImpl for TreeViewObject {} 80 | -------------------------------------------------------------------------------- /src/grid.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{Index, IndexMut}; 2 | use std::rc::Rc; 3 | 4 | use fnv::FnvHashMap; 5 | 6 | use nvim_rs::Value; 7 | 8 | use crate::highlight::{Highlight, HighlightMap}; 9 | use crate::ui_model::{ModelRect, 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 | /// Flush any pending cursor position updates (e.g. updates we received before getting a 'flush' 70 | /// event) 71 | pub fn flush_cursor(&mut self) { 72 | for grid in self.grids.values_mut() { 73 | grid.flush_cursor(); 74 | } 75 | } 76 | } 77 | 78 | pub struct Grid { 79 | model: UiModel, 80 | } 81 | 82 | impl Grid { 83 | pub fn new() -> Self { 84 | Grid { 85 | model: UiModel::default(), 86 | } 87 | } 88 | 89 | pub fn get_cursor(&self) -> (usize, usize) { 90 | self.model.get_real_cursor() 91 | } 92 | 93 | pub fn flush_cursor(&mut self) { 94 | self.model.flush_cursor(); 95 | } 96 | 97 | pub fn cur_point(&self) -> ModelRect { 98 | self.model.cur_real_point() 99 | } 100 | 101 | pub fn resize(&mut self, columns: u64, rows: u64) { 102 | if self.model.columns != columns as usize || self.model.rows != rows as usize { 103 | self.model = UiModel::new(rows, columns); 104 | } 105 | } 106 | 107 | pub fn cursor_goto(&mut self, row: usize, col: usize) { 108 | self.model.set_cursor(row, col) 109 | } 110 | 111 | pub fn clear(&mut self, default_hl: &Rc) { 112 | self.model.clear(default_hl); 113 | } 114 | 115 | #[allow(clippy::get_first)] // get(0), get(1), get(2) more consistent than .first(), .get(1) 116 | pub fn line( 117 | &mut self, 118 | row: usize, 119 | col_start: usize, 120 | cells: Vec>, 121 | highlights: &HighlightMap, 122 | ) -> ModelRect { 123 | let mut hl_id = None; 124 | let mut col_end = col_start; 125 | 126 | for cell in cells { 127 | let ch = cell.get(0).unwrap().as_str().unwrap_or(""); 128 | hl_id = cell.get(1).and_then(|h| h.as_u64()).or(hl_id); 129 | let repeat = cell.get(2).and_then(|r| r.as_u64()).unwrap_or(1) as usize; 130 | 131 | self.model.put( 132 | row, 133 | col_end, 134 | ch, 135 | ch.is_empty(), 136 | repeat, 137 | highlights.get(hl_id), 138 | ); 139 | col_end += repeat; 140 | } 141 | 142 | ModelRect::new(row, row, col_start, col_end - 1) 143 | } 144 | 145 | pub fn scroll( 146 | &mut self, 147 | top: u64, 148 | bot: u64, 149 | left: u64, 150 | right: u64, 151 | rows: i64, 152 | _: i64, 153 | default_hl: &Rc, 154 | ) { 155 | self.model.scroll( 156 | top as i64, 157 | bot as i64 - 1, 158 | left as usize, 159 | right as usize - 1, 160 | rows, 161 | default_hl, 162 | ) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/input.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use log::debug; 4 | 5 | use crate::nvim::{ErrorReport, NvimSession}; 6 | 7 | include!(concat!(env!("OUT_DIR"), "/key_map_table.rs")); 8 | 9 | pub fn keyval_to_input_string(in_str: &str, in_state: gdk::ModifierType) -> String { 10 | let mut val = in_str; 11 | let mut state = in_state; 12 | let empty = in_str.is_empty(); 13 | 14 | if !empty { 15 | debug!("keyval -> {}", in_str); 16 | } 17 | 18 | // CTRL-^ and CTRL-@ don't work in the normal way. 19 | if state.contains(gdk::ModifierType::CONTROL_MASK) 20 | && !state.contains(gdk::ModifierType::SHIFT_MASK) 21 | && !state.contains(gdk::ModifierType::ALT_MASK) 22 | && !state.contains(gdk::ModifierType::META_MASK) 23 | { 24 | if val == "6" { 25 | val = "^"; 26 | } else if val == "2" { 27 | val = "@"; 28 | } 29 | } 30 | 31 | let chars: Vec = in_str.chars().collect(); 32 | 33 | if chars.len() == 1 { 34 | let ch = chars[0]; 35 | 36 | // Remove SHIFT 37 | if ch.is_ascii() && !ch.is_alphanumeric() { 38 | state.remove(gdk::ModifierType::SHIFT_MASK); 39 | } 40 | } 41 | 42 | if val == "<" { 43 | val = "lt"; 44 | } 45 | 46 | let mut mod_chars = Vec::<&str>::with_capacity(3); 47 | if state.contains(gdk::ModifierType::SHIFT_MASK) { 48 | mod_chars.push("S"); 49 | } 50 | if state.contains(gdk::ModifierType::CONTROL_MASK) { 51 | mod_chars.push("C"); 52 | } 53 | if state.contains(gdk::ModifierType::ALT_MASK) || state.contains(gdk::ModifierType::META_MASK) { 54 | mod_chars.push("A"); 55 | } 56 | 57 | let sep = if empty { "" } else { "-" }; 58 | let input = [mod_chars.as_slice(), &[val]].concat().join(sep); 59 | 60 | if !empty && input.chars().count() > 1 { 61 | format!("<{input}>") 62 | } else { 63 | input 64 | } 65 | } 66 | 67 | pub fn convert_key(keyval: gdk::Key, modifiers: gdk::ModifierType) -> Option { 68 | if let Some(ref keyval_name) = keyval.name() { 69 | if let Some(cnvt) = KEYVAL_MAP.get(keyval_name.as_str()).cloned() { 70 | return Some(keyval_to_input_string(cnvt, modifiers)); 71 | } 72 | } 73 | 74 | keyval 75 | .to_unicode() 76 | .map(|ch| keyval_to_input_string(&ch.to_string(), modifiers)) 77 | } 78 | 79 | pub fn im_input(nvim: &NvimSession, input: &str) { 80 | debug!("nvim_input -> {}", input); 81 | 82 | let input: String = input 83 | .chars() 84 | .map(|ch| keyval_to_input_string(&ch.to_string(), gdk::ModifierType::empty())) 85 | .collect(); 86 | nvim.block_timeout(nvim.input(&input)) 87 | .ok_and_report() 88 | .expect("Failed to send input command to nvim"); 89 | } 90 | 91 | pub fn gtk_key_press( 92 | nvim: &NvimSession, 93 | keyval: gdk::Key, 94 | modifiers: gdk::ModifierType, 95 | ) -> glib::Propagation { 96 | if let Some(input) = convert_key(keyval, modifiers) { 97 | debug!("nvim_input -> {}", input); 98 | nvim.block_timeout(nvim.input(&input)) 99 | .ok_and_report() 100 | .expect("Failed to send input command to nvim"); 101 | glib::Propagation::Stop 102 | } else { 103 | glib::Propagation::Proceed 104 | } 105 | } 106 | 107 | #[cfg(test)] 108 | mod tests { 109 | use super::*; 110 | 111 | #[test] 112 | fn test_keyval_to_input_string() { 113 | macro_rules! test { 114 | ( $( $in_str:literal $( , $( $mod:ident )|* )? == $out_str:literal );*; ) => { 115 | let mut modifier; 116 | $( 117 | modifier = gdk::ModifierType::empty() $( | $( gdk::ModifierType::$mod )|* )?; 118 | assert_eq!(keyval_to_input_string($in_str, modifier), $out_str) 119 | );* 120 | } 121 | } 122 | 123 | test! { 124 | "a" == "a"; 125 | "" == ""; 126 | "6" == "6"; 127 | "2" == "2"; 128 | "<" == ""; 129 | "", SHIFT_MASK == "S"; 130 | "", SHIFT_MASK | CONTROL_MASK | ALT_MASK == "SCA"; 131 | "a", SHIFT_MASK == ""; 132 | "a", SHIFT_MASK | CONTROL_MASK | ALT_MASK == ""; 133 | "6", CONTROL_MASK == ""; 134 | "6", CONTROL_MASK | META_MASK == ""; 135 | "2", CONTROL_MASK == ""; 136 | "2", CONTROL_MASK | ALT_MASK == ""; 137 | "j", SUPER_MASK == "j"; 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/misc.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::mem; 3 | 4 | use once_cell::sync::Lazy; 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(filename: &str) -> Cow { 41 | static SPECIAL_CHARS: Lazy = Lazy::new(|| { 42 | 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 | static SLASH: Lazy = Lazy::new(|| Regex::new(r"/").unwrap()); 65 | Some(String::from(SLASH.replace_all(&path, r"\"))) 66 | } else { 67 | Some("/".to_owned() + &path) 68 | } 69 | } 70 | 71 | /// info text 72 | pub fn about_comments() -> String { 73 | format!( 74 | "Build on top of neovim\n\ 75 | Minimum supported neovim version: {}", 76 | shell::MINIMUM_SUPPORTED_NVIM_VERSION 77 | ) 78 | } 79 | 80 | /// Escape a VimL expression so that it may included in quotes 81 | #[rustfmt::skip] 82 | pub fn viml_escape(viml: &str) -> String { 83 | viml.replace('\\', r"\\") 84 | .replace('"', r#"\""#) 85 | } 86 | 87 | pub trait BoolExt { 88 | /// Parse a bool in a str represented by '0' or '1' 89 | fn from_int_str(int_str: &str) -> Option; 90 | } 91 | 92 | impl BoolExt for bool { 93 | fn from_int_str(int_str: &str) -> Option { 94 | match int_str { 95 | "0" => Some(false), 96 | "1" => Some(true), 97 | _ => None, 98 | } 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | 106 | #[test] 107 | fn test_comma_split() { 108 | let res = split_at_comma("a,b"); 109 | assert_eq!(2, res.len()); 110 | assert_eq!("a", res[0]); 111 | assert_eq!("b", res[1]); 112 | 113 | let res = split_at_comma("a,b\\,c"); 114 | assert_eq!(2, res.len()); 115 | assert_eq!("a", res[0]); 116 | assert_eq!("b,c", res[1]); 117 | } 118 | 119 | #[test] 120 | fn test_viml_escape() { 121 | assert_eq!(r#"a\"b\\"#, viml_escape(r#"a"b\"#)); 122 | } 123 | 124 | #[test] 125 | fn test_bool_int_str() { 126 | assert_eq!(bool::from_int_str("0"), Some(false)); 127 | assert_eq!(bool::from_int_str("1"), Some(true)); 128 | assert_eq!(bool::from_int_str("2"), None); 129 | assert_eq!(bool::from_int_str("f"), None); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/mode.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use nvim_rs::Value; 3 | use std::collections::HashMap; 4 | 5 | #[derive(Clone, PartialEq, Eq)] 6 | pub enum NvimMode { 7 | Normal, 8 | Insert, 9 | Other, 10 | } 11 | 12 | pub struct Mode { 13 | mode: NvimMode, 14 | idx: usize, 15 | info: Option>, 16 | } 17 | 18 | impl Mode { 19 | pub fn new() -> Self { 20 | Mode { 21 | mode: NvimMode::Normal, 22 | idx: 0, 23 | info: None, 24 | } 25 | } 26 | 27 | pub fn is(&self, mode: &NvimMode) -> bool { 28 | self.mode == *mode 29 | } 30 | 31 | pub fn mode_info(&self) -> Option<&ModeInfo> { 32 | self.info.as_ref().and_then(|i| i.get(self.idx)) 33 | } 34 | 35 | pub fn update(&mut self, mode: &str, idx: usize) { 36 | match mode { 37 | "normal" => self.mode = NvimMode::Normal, 38 | "insert" => self.mode = NvimMode::Insert, 39 | _ => self.mode = NvimMode::Other, 40 | } 41 | 42 | self.idx = idx; 43 | } 44 | 45 | pub fn set_info(&mut self, cursor_style_enabled: bool, info: Vec) { 46 | self.info = if cursor_style_enabled { 47 | Some(info) 48 | } else { 49 | None 50 | }; 51 | } 52 | } 53 | 54 | #[derive(Debug, PartialEq, Eq, 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, Eq, 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 98 | .get("cell_percentage") 99 | .and_then(|cp| cp.as_u64()), 100 | blinkwait: mode_info_map 101 | .get("blinkwait") 102 | .and_then(|cp| cp.as_u64()) 103 | .map(|v| v as u32), 104 | }) 105 | } 106 | 107 | pub fn cursor_shape(&self) -> Option<&CursorShape> { 108 | self.cursor_shape.as_ref() 109 | } 110 | 111 | pub fn cell_percentage(&self) -> u64 { 112 | self.cell_percentage.unwrap_or(0) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/nvim/client.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc, sync::RwLock}; 2 | 3 | use crate::nvim::*; 4 | 5 | #[derive(Default)] 6 | pub struct NeovimApiInfo { 7 | pub channel: i64, 8 | 9 | pub ext_cmdline: bool, 10 | pub ext_wildmenu: bool, 11 | pub ext_hlstate: bool, 12 | pub ext_linegrid: bool, 13 | pub ext_popupmenu: bool, 14 | pub ext_tabline: bool, 15 | pub ext_termcolors: bool, 16 | 17 | pub ui_pum_set_height: bool, 18 | pub ui_pum_set_bounds: bool, 19 | } 20 | 21 | impl NeovimApiInfo { 22 | pub fn new(api_info: Vec) -> Result { 23 | let mut self_ = Self::default(); 24 | let mut api_info = api_info.into_iter(); 25 | 26 | self_.channel = api_info 27 | .next() 28 | .ok_or("Channel is missing")? 29 | .as_i64() 30 | .ok_or("Channel is not i64")?; 31 | 32 | let metadata = match api_info.next().ok_or("Metadata is missing")? { 33 | Value::Map(pairs) => Ok(pairs), 34 | v => Err(format!("Metadata is wrong type, got {v:?}")), 35 | }?; 36 | 37 | for (key, value) in metadata.into_iter() { 38 | match key 39 | .as_str() 40 | .ok_or(format!("Metadata key {key:?} isn't string"))? 41 | { 42 | "ui_options" => self_.parse_ui_options(value)?, 43 | "functions" => self_.parse_functions(value)?, 44 | _ => (), 45 | } 46 | } 47 | Ok(self_) 48 | } 49 | 50 | #[inline] 51 | fn parse_ui_options(&mut self, extensions: Value) -> Result<(), String> { 52 | for extension in extensions 53 | .as_array() 54 | .ok_or(format!("UI option list is invalid: {extensions:?}"))? 55 | { 56 | match extension 57 | .as_str() 58 | .ok_or(format!("UI option isn't string: {extensions:?}"))? 59 | { 60 | "ext_cmdline" => self.ext_cmdline = true, 61 | "ext_wildmenu" => self.ext_wildmenu = true, 62 | "ext_hlstate" => self.ext_hlstate = true, 63 | "ext_linegrid" => self.ext_linegrid = true, 64 | "ext_popupmenu" => self.ext_popupmenu = true, 65 | "ext_tabline" => self.ext_tabline = true, 66 | "ext_termcolors" => self.ext_termcolors = true, 67 | _ => (), 68 | }; 69 | } 70 | Ok(()) 71 | } 72 | 73 | #[inline] 74 | fn parse_functions(&mut self, functions: Value) -> Result<(), String> { 75 | for function in functions 76 | .as_array() 77 | .ok_or_else(|| format!("Function list is not a list: {functions:?}"))? 78 | { 79 | match function 80 | .as_map() 81 | .ok_or_else(|| format!("Function info is not a map: {function:?}"))? 82 | .iter() 83 | .find_map(|(key, value)| { 84 | key.as_str() 85 | .filter(|k| *k == "name") 86 | .and_then(|_| value.as_str()) 87 | }) 88 | .ok_or_else(|| format!("Function info is missing name: {functions:?}"))? 89 | { 90 | "nvim_ui_pum_set_height" => self.ui_pum_set_height = true, 91 | "nvim_ui_pum_set_bounds" => self.ui_pum_set_bounds = true, 92 | _ => (), 93 | } 94 | } 95 | Ok(()) 96 | } 97 | } 98 | 99 | #[derive(Clone, Copy, PartialEq)] 100 | enum NeovimClientStatus { 101 | Uninitialized, 102 | InitInProgress, 103 | Initialized, 104 | Error, 105 | } 106 | 107 | struct NeovimClientState { 108 | status: NeovimClientStatus, 109 | api_info: Option>, 110 | } 111 | 112 | pub struct NeovimClient { 113 | state: RefCell, 114 | nvim: RwLock>, 115 | } 116 | 117 | impl NeovimClient { 118 | pub fn new() -> Self { 119 | NeovimClient { 120 | state: RefCell::new(NeovimClientState { 121 | status: NeovimClientStatus::Uninitialized, 122 | api_info: None, 123 | }), 124 | nvim: RwLock::new(None), 125 | } 126 | } 127 | 128 | pub fn clear(&self) { 129 | *self.nvim.write().unwrap() = None 130 | } 131 | 132 | pub fn set(&self, nvim: NvimSession) { 133 | self.nvim.write().unwrap().replace(nvim); 134 | } 135 | 136 | pub fn api_info(&self) -> Option> { 137 | self.state.borrow().api_info.as_ref().cloned() 138 | } 139 | 140 | pub fn set_initialized(&self, api_info: NeovimApiInfo) { 141 | let mut state = self.state.borrow_mut(); 142 | 143 | state.status = NeovimClientStatus::Initialized; 144 | state.api_info = Some(Rc::new(api_info)); 145 | } 146 | 147 | pub fn set_error(&self) { 148 | self.state.borrow_mut().status = NeovimClientStatus::Error; 149 | } 150 | 151 | pub fn set_in_progress(&self) { 152 | self.state.borrow_mut().status = NeovimClientStatus::InitInProgress; 153 | } 154 | 155 | pub fn is_initialized(&self) -> bool { 156 | self.state.borrow().status == NeovimClientStatus::Initialized 157 | } 158 | 159 | pub fn is_uninitialized(&self) -> bool { 160 | self.state.borrow().status == NeovimClientStatus::Uninitialized 161 | } 162 | 163 | pub fn is_initializing(&self) -> bool { 164 | self.state.borrow().status == NeovimClientStatus::InitInProgress 165 | } 166 | 167 | pub fn nvim(&self) -> Option { 168 | self.nvim.read().unwrap().clone() 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/nvim/ext.rs: -------------------------------------------------------------------------------- 1 | use std::convert::{TryFrom, TryInto}; 2 | use std::error::Error; 3 | 4 | use log::error; 5 | 6 | use nvim_rs::error::CallError; 7 | 8 | use crate::nvim::{NvimSession, SessionError}; 9 | 10 | pub trait CallErrorExt { 11 | fn print(&self); 12 | } 13 | impl CallErrorExt for CallError { 14 | fn print(&self) { 15 | error!("Error in last Neovim request: {}", self); 16 | error!("Caused by: {:?}", self.source()); 17 | } 18 | } 19 | 20 | pub trait ErrorReport { 21 | fn report_err(&self); 22 | 23 | fn ok_and_report(self) -> Option; 24 | } 25 | 26 | impl ErrorReport for Result> { 27 | fn report_err(&self) { 28 | if let Err(err) = self { 29 | err.print(); 30 | } 31 | } 32 | 33 | fn ok_and_report(self) -> Option { 34 | self.report_err(); 35 | Some(self.unwrap()) 36 | } 37 | } 38 | 39 | impl ErrorReport for Result { 40 | fn report_err(&self) { 41 | if let Err(ref err) = self { 42 | match *err { 43 | SessionError::CallError(ref e) => e.print(), 44 | SessionError::TimeoutError(ref e) => { 45 | panic!("Neovim request {:?} timed out", e.source()); 46 | } 47 | } 48 | } 49 | } 50 | 51 | fn ok_and_report(self) -> Option { 52 | self.report_err(); 53 | self.ok() 54 | } 55 | } 56 | 57 | /// An error from an neovim request that is part of neovim's regular operation, potentially with an 58 | /// error message that's intended to potentially be displayed to the user as an error in the 59 | /// messages buffer 60 | #[derive(PartialEq, Eq, Debug)] 61 | pub enum NormalError<'a> { 62 | /// A neovim request was interrupted by the user 63 | KeyboardInterrupt, 64 | /// A neovim error with a message for the messages buffer 65 | Message { 66 | source: &'a str, 67 | message: &'a str, 68 | code: u32, 69 | }, 70 | } 71 | 72 | impl NormalError<'_> { 73 | /// Print an error message to neovim's message buffer, if we have one 74 | pub async fn print(&self, nvim: &NvimSession) { 75 | if let Self::Message { message, .. } = self { 76 | // TODO: Figure out timeout situation, in the mean time just disable timeouts here 77 | if let Err(e) = nvim.err_writeln(message).await { 78 | error!( 79 | "Failed to print error message \"{:?}\" in nvim: {}", 80 | self, e 81 | ); 82 | } 83 | } 84 | } 85 | 86 | /// Check if this error has the given code 87 | pub fn has_code(&self, code: u32) -> bool { 88 | match self { 89 | Self::Message { code: our_code, .. } => *our_code == code, 90 | _ => false, 91 | } 92 | } 93 | } 94 | 95 | impl<'a> TryFrom<&'a CallError> for NormalError<'a> { 96 | type Error = (); 97 | 98 | fn try_from(err: &'a CallError) -> Result { 99 | if let CallError::NeovimError(code, message) = err { 100 | if *code != Some(0) { 101 | return Err(()); 102 | } 103 | 104 | if message == "Keyboard interrupt" { 105 | return Ok(Self::KeyboardInterrupt); 106 | } else if let Some(message) = message.strip_prefix("Vim(") { 107 | let (source, message) = match message.split_once("):") { 108 | Some((source, message)) => (source, message), 109 | None => return Err(()), 110 | }; 111 | 112 | let code = match message 113 | .strip_prefix('E') 114 | .and_then(|message| message.split_once(':')) 115 | .map(|(message, _)| message) 116 | .and_then(|message| message.parse::().ok()) 117 | { 118 | Some(code) => code, 119 | None => return Err(()), 120 | }; 121 | 122 | return Ok(Self::Message { 123 | source, 124 | message, 125 | code, 126 | }); 127 | } 128 | } 129 | 130 | Err(()) 131 | } 132 | } 133 | 134 | impl<'a> TryFrom<&'a SessionError> for NormalError<'a> { 135 | type Error = (); 136 | 137 | fn try_from(err: &'a SessionError) -> Result { 138 | if let SessionError::CallError(err) = err { 139 | err.as_ref().try_into() 140 | } else { 141 | Err(()) 142 | } 143 | } 144 | } 145 | 146 | #[cfg(test)] 147 | mod tests { 148 | use super::*; 149 | 150 | #[test] 151 | fn test_normal_error() { 152 | macro_rules! test { 153 | ( $( $in_str:literal == $expr:expr );*; ) => { 154 | let mut error; 155 | $( 156 | error = CallError::NeovimError(Some(0), $in_str.into()); 157 | assert_eq!(NormalError::try_from(&error), $expr) 158 | );* 159 | } 160 | } 161 | 162 | test! { 163 | "Vim(source):E325: ATTENTION" == 164 | Ok(NormalError::Message { 165 | source: "source", 166 | message: "E325: ATTENTION", 167 | code: 325, 168 | }); 169 | "source): E325: ATTENTION" == Err(()); // Missing Vim( 170 | "Vim(source:E325: ATTENTION" == Err(()); // 1st : should be ): 171 | "Vim(source):EXXX: ATTENTION" == Err(()); // Invalid error code 172 | "Vim(source):325: ATTENTION" == Err(()); // Missing E prefix 173 | "Vim(source)E325: ATTENTION" == Err(()); // Missing 1st : 174 | "Vim(source):E325 ATTENTION" == Err(()); // Missing 2nd : 175 | "Keyboard interrupt" == Ok(NormalError::KeyboardInterrupt); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/nvim/handler.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | result, 3 | sync::{mpsc, Arc}, 4 | }; 5 | 6 | use log::{debug, error}; 7 | 8 | use nvim_rs::{compat::tokio::Compat, Handler, Value}; 9 | 10 | use async_trait::async_trait; 11 | 12 | use crate::nvim::{Neovim, NvimWriter}; 13 | use crate::shell; 14 | use crate::ui::UiMutex; 15 | 16 | use super::redraw_handler::{self, PendingPopupMenu, RedrawMode}; 17 | 18 | pub struct NvimHandler { 19 | shell: Arc>, 20 | resize_status: Arc, 21 | } 22 | 23 | impl NvimHandler { 24 | pub fn new(shell: Arc>, resize_status: Arc) -> Self { 25 | NvimHandler { 26 | shell, 27 | resize_status, 28 | } 29 | } 30 | 31 | async fn nvim_cb(&self, method: String, params: Vec) { 32 | match method.as_ref() { 33 | "redraw" => self.safe_call(move |ui| call_redraw_handler(params, ui)), 34 | "Gui" => { 35 | if !params.is_empty() { 36 | let mut params_iter = params.into_iter(); 37 | if let Some(ev_name) = params_iter.next() { 38 | if let Value::String(ev_name) = ev_name { 39 | let args = params_iter.collect(); 40 | self.safe_call(move |ui| { 41 | let ui = &mut ui.borrow_mut(); 42 | redraw_handler::call_gui_event( 43 | ui, 44 | ev_name.as_str().ok_or("Event name does not exists")?, 45 | args, 46 | )?; 47 | ui.queue_draw(RedrawMode::All); 48 | Ok(()) 49 | }); 50 | } else { 51 | error!("Unsupported event"); 52 | } 53 | } else { 54 | error!("Event name does not exists"); 55 | } 56 | } else { 57 | error!("Unsupported event {:?}", params); 58 | } 59 | } 60 | "subscription" => { 61 | self.safe_call(move |ui| { 62 | let ui = &ui.borrow(); 63 | ui.notify(params) 64 | }); 65 | } 66 | "resized" => { 67 | debug!("Received resized notification"); 68 | self.resize_status.notify_finished(); 69 | } 70 | _ => { 71 | error!("Notification {}({:?})", method, params); 72 | } 73 | } 74 | } 75 | 76 | fn nvim_cb_req(&self, method: String, params: Vec) -> result::Result { 77 | match method.as_ref() { 78 | "Gui" => { 79 | if !params.is_empty() { 80 | let mut params_iter = params.into_iter(); 81 | if let Some(req_name) = params_iter.next() { 82 | if let Value::String(req_name) = req_name { 83 | let args = params_iter.collect(); 84 | let (sender, receiver) = mpsc::channel(); 85 | self.safe_call(move |ui| { 86 | sender 87 | .send(redraw_handler::call_gui_request( 88 | &ui.clone(), 89 | req_name.as_str().ok_or("Event name does not exists")?, 90 | &args, 91 | )) 92 | .unwrap(); 93 | { 94 | let ui = &mut ui.borrow_mut(); 95 | ui.queue_draw(RedrawMode::All); 96 | } 97 | Ok(()) 98 | }); 99 | Ok(receiver.recv().unwrap()?) 100 | } else { 101 | error!("Unsupported request"); 102 | Err(Value::Nil) 103 | } 104 | } else { 105 | error!("Request name does not exist"); 106 | Err(Value::Nil) 107 | } 108 | } else { 109 | error!("Unsupported request {:?}", params); 110 | Err(Value::Nil) 111 | } 112 | } 113 | _ => { 114 | error!("Request {}({:?})", method, params); 115 | Err(Value::Nil) 116 | } 117 | } 118 | } 119 | 120 | fn safe_call(&self, cb: F) 121 | where 122 | F: FnOnce(&Arc>) -> result::Result<(), String> + 'static + Send, 123 | { 124 | safe_call(self.shell.clone(), cb); 125 | } 126 | } 127 | 128 | fn call_redraw_handler( 129 | params: Vec, 130 | ui: &Arc>, 131 | ) -> result::Result<(), String> { 132 | let mut repaint_mode = RedrawMode::Nothing; 133 | let mut pending_popupmenu = PendingPopupMenu::None; 134 | 135 | let mut ui_ref = ui.borrow_mut(); 136 | for ev in params { 137 | let ev_args = match ev { 138 | Value::Array(args) => args, 139 | _ => { 140 | error!("Unsupported event type: {:?}", ev); 141 | continue; 142 | } 143 | }; 144 | let mut args_iter = ev_args.into_iter(); 145 | let ev_name = match args_iter.next() { 146 | Some(ev_name) => ev_name, 147 | None => { 148 | error!( 149 | "No name provided with redraw event, args: {:?}", 150 | args_iter.as_slice() 151 | ); 152 | continue; 153 | } 154 | }; 155 | let ev_name = match ev_name.as_str() { 156 | Some(ev_name) => ev_name, 157 | None => { 158 | error!( 159 | "Expected event name to be str, instead got {:?}. Args: {:?}", 160 | ev_name, 161 | args_iter.as_slice() 162 | ); 163 | continue; 164 | } 165 | }; 166 | 167 | for local_args in args_iter { 168 | let args = match local_args { 169 | Value::Array(ar) => ar, 170 | _ => vec![], 171 | }; 172 | 173 | let (call_repaint_mode, call_popupmenu) = 174 | match redraw_handler::call(&mut ui_ref, ev_name, args) { 175 | Ok(mode) => mode, 176 | Err(desc) => return Err(format!("Event {ev_name}\n{desc}")), 177 | }; 178 | repaint_mode = repaint_mode.max(call_repaint_mode); 179 | pending_popupmenu.update(call_popupmenu); 180 | } 181 | } 182 | 183 | ui_ref.queue_draw(repaint_mode); 184 | drop(ui_ref); 185 | ui.borrow().popupmenu_flush(pending_popupmenu); 186 | Ok(()) 187 | } 188 | 189 | fn safe_call(shell: Arc>, cb: F) 190 | where 191 | F: FnOnce(&Arc>) -> result::Result<(), String> + 'static + Send, 192 | { 193 | let mut cb = Some(cb); 194 | glib::idle_add_once(move || { 195 | if let Err(msg) = cb.take().unwrap()(&shell) { 196 | error!("Error call function: {}", msg); 197 | } 198 | }); 199 | } 200 | 201 | impl Clone for NvimHandler { 202 | fn clone(&self) -> Self { 203 | NvimHandler { 204 | shell: self.shell.clone(), 205 | resize_status: self.resize_status.clone(), 206 | } 207 | } 208 | } 209 | 210 | #[async_trait] 211 | impl Handler for NvimHandler { 212 | type Writer = Compat; 213 | 214 | async fn handle_notify(&self, name: String, args: Vec, _: Neovim) { 215 | self.nvim_cb(name, args).await; 216 | } 217 | 218 | async fn handle_request( 219 | &self, 220 | name: String, 221 | args: Vec, 222 | _: Neovim, 223 | ) -> result::Result { 224 | self.nvim_cb_req(name, args) 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/nvim_config.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{remove_file, OpenOptions}; 2 | use std::io::Write; 3 | use std::path::PathBuf; 4 | 5 | use log::{debug, error}; 6 | 7 | use crate::dirs; 8 | use crate::plug_manager; 9 | 10 | #[derive(Clone)] 11 | pub struct NvimConfig { 12 | plug_config: Option, 13 | } 14 | 15 | impl NvimConfig { 16 | const CONFIG_PATH: &'static str = "settings.vim"; 17 | 18 | pub fn new(plug_config: Option) -> Self { 19 | NvimConfig { plug_config } 20 | } 21 | 22 | pub fn generate_config(&self) -> Option { 23 | if self.plug_config.is_some() { 24 | match self.write_file() { 25 | Err(err) => { 26 | error!("{}", err); 27 | None 28 | } 29 | Ok(file) => Some(file), 30 | } 31 | } else { 32 | NvimConfig::config_path().map(remove_file); 33 | None 34 | } 35 | } 36 | 37 | pub fn config_path() -> Option { 38 | let mut path = dirs::app_config_dir().to_path_buf(); 39 | path.push(NvimConfig::CONFIG_PATH); 40 | if path.is_file() { 41 | return Some(path); 42 | } 43 | 44 | None 45 | } 46 | 47 | fn write_file(&self) -> Result { 48 | let mut config_dir = dirs::app_config_dir_create()?; 49 | config_dir.push(NvimConfig::CONFIG_PATH); 50 | 51 | let mut file = OpenOptions::new() 52 | .create(true) 53 | .write(true) 54 | .truncate(true) 55 | .open(&config_dir) 56 | .map_err(|e| format!("{e}"))?; 57 | 58 | let content = &self.plug_config.as_ref().unwrap().source; 59 | if !content.is_empty() { 60 | debug!("{}", content); 61 | file.write_all(content.as_bytes()) 62 | .map_err(|e| format!("{e}"))?; 63 | } 64 | 65 | file.sync_all().map_err(|e| format!("{e}"))?; 66 | Ok(config_dir) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/nvim_viewport.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | 3 | use gtk::{graphene::Rect, prelude::*, subclass::prelude::*}; 4 | 5 | use std::{ 6 | cell::RefCell, 7 | sync::{Arc, Weak}, 8 | }; 9 | 10 | use crate::{ 11 | popup_menu::PopupMenuPopover, 12 | render::*, 13 | shell::{RenderState, State}, 14 | ui::UiMutex, 15 | }; 16 | 17 | glib::wrapper! { 18 | pub struct NvimViewport(ObjectSubclass) 19 | @extends gtk::Widget, 20 | @implements gtk::Accessible; 21 | } 22 | 23 | impl NvimViewport { 24 | pub fn new() -> Self { 25 | glib::Object::new::() 26 | } 27 | 28 | pub fn set_shell_state(&self, state: &Arc>) { 29 | self.set_property("shell-state", glib::BoxedAnyObject::new(state.clone())); 30 | } 31 | 32 | pub fn set_context_menu(&self, popover_menu: >k::PopoverMenu) { 33 | self.set_property("context-menu", popover_menu); 34 | } 35 | 36 | pub fn set_completion_popover(&self, completion_popover: &PopupMenuPopover) { 37 | self.set_property("completion-popover", completion_popover); 38 | } 39 | 40 | pub fn set_ext_cmdline(&self, ext_cmdline: >k::Popover) { 41 | self.set_property("ext-cmdline", ext_cmdline); 42 | } 43 | 44 | pub fn clear_snapshot_cache(&self) { 45 | self.set_property("snapshot-cached", false); 46 | } 47 | } 48 | 49 | /** The inner state structure for the viewport widget, for holding non-glib types (e.g. ones that 50 | * need inferior mutability) */ 51 | #[derive(Default)] 52 | struct NvimViewportInner { 53 | state: Weak>, 54 | snapshot_cache: Option, 55 | } 56 | 57 | #[derive(Default)] 58 | pub struct NvimViewportObject { 59 | inner: RefCell, 60 | context_menu: glib::WeakRef, 61 | completion_popover: glib::WeakRef, 62 | ext_cmdline: glib::WeakRef, 63 | } 64 | 65 | #[glib::object_subclass] 66 | impl ObjectSubclass for NvimViewportObject { 67 | const NAME: &'static str = "NvimViewport"; 68 | type Type = NvimViewport; 69 | type ParentType = gtk::Widget; 70 | 71 | fn class_init(klass: &mut Self::Class) { 72 | klass.set_css_name("widget"); 73 | klass.set_accessible_role(gtk::AccessibleRole::TextBox); 74 | } 75 | } 76 | 77 | impl ObjectImpl for NvimViewportObject { 78 | fn dispose(&self) { 79 | if let Some(popover_menu) = self.context_menu.upgrade() { 80 | popover_menu.unparent(); 81 | } 82 | if let Some(completion_popover) = self.completion_popover.upgrade() { 83 | completion_popover.unparent(); 84 | } 85 | if let Some(ext_cmdline) = self.ext_cmdline.upgrade() { 86 | ext_cmdline.unparent(); 87 | } 88 | } 89 | 90 | fn properties() -> &'static [glib::ParamSpec] { 91 | static PROPERTIES: Lazy> = Lazy::new(|| { 92 | vec![ 93 | glib::ParamSpecObject::builder::("shell-state") 94 | .write_only() 95 | .build(), 96 | glib::ParamSpecBoolean::builder("snapshot-cached").build(), 97 | glib::ParamSpecObject::builder::("context-menu").build(), 98 | glib::ParamSpecObject::builder::("completion-popover").build(), 99 | glib::ParamSpecObject::builder::("ext-cmdline").build(), 100 | ] 101 | }); 102 | 103 | PROPERTIES.as_ref() 104 | } 105 | 106 | fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 107 | let obj = self.obj(); 108 | match pspec.name() { 109 | "shell-state" => { 110 | let mut inner = self.inner.borrow_mut(); 111 | debug_assert!(inner.state.upgrade().is_none()); 112 | 113 | inner.state = 114 | Arc::downgrade(&value.get::().unwrap().borrow()); 115 | } 116 | "snapshot-cached" => { 117 | if !value.get::().unwrap() { 118 | self.inner.borrow_mut().snapshot_cache = None; 119 | } 120 | } 121 | "context-menu" => { 122 | if let Some(context_menu) = self.context_menu.upgrade() { 123 | context_menu.unparent(); 124 | } 125 | let context_menu: gtk::PopoverMenu = value.get().unwrap(); 126 | 127 | context_menu.set_parent(&*obj); 128 | self.context_menu.set(Some(&context_menu)); 129 | } 130 | "completion-popover" => { 131 | if let Some(popover) = self.completion_popover.upgrade() { 132 | popover.unparent(); 133 | } 134 | let popover: PopupMenuPopover = value.get().unwrap(); 135 | 136 | popover.set_parent(&*obj); 137 | self.completion_popover.set(Some(&popover)); 138 | } 139 | "ext-cmdline" => { 140 | if let Some(ext_cmdline) = self.ext_cmdline.upgrade() { 141 | ext_cmdline.unparent(); 142 | } 143 | let ext_cmdline: Option = value.get().unwrap(); 144 | 145 | if let Some(ref ext_cmdline) = ext_cmdline { 146 | ext_cmdline.set_parent(&*obj); 147 | } 148 | self.ext_cmdline.set(ext_cmdline.as_ref()); 149 | } 150 | _ => unreachable!(), 151 | } 152 | } 153 | 154 | fn property(&self, _id: usize, pspec: &glib::ParamSpec) -> glib::Value { 155 | match pspec.name() { 156 | "snapshot-cached" => self.inner.borrow().snapshot_cache.is_some().to_value(), 157 | "context-menu" => self.context_menu.upgrade().to_value(), 158 | "completion-popover" => self.completion_popover.upgrade().to_value(), 159 | "ext-cmdline" => self.ext_cmdline.upgrade().to_value(), 160 | _ => unreachable!(), 161 | } 162 | } 163 | } 164 | 165 | impl WidgetImpl for NvimViewportObject { 166 | fn size_allocate(&self, width: i32, height: i32, baseline: i32) { 167 | self.parent_size_allocate(width, height, baseline); 168 | if let Some(context_menu) = self.context_menu.upgrade() { 169 | context_menu.present(); 170 | } 171 | if let Some(completion_popover) = self.completion_popover.upgrade() { 172 | completion_popover.present(); 173 | } 174 | if let Some(ext_cmdline) = self.ext_cmdline.upgrade() { 175 | ext_cmdline.present(); 176 | } 177 | 178 | let inner = self.inner.borrow(); 179 | if let Some(state) = inner.state.upgrade() { 180 | state.borrow_mut().try_nvim_resize(); 181 | } 182 | } 183 | 184 | fn snapshot(&self, snapshot_in: >k::Snapshot) { 185 | let obj = self.obj(); 186 | let mut inner = self.inner.borrow_mut(); 187 | let state = match inner.state.upgrade() { 188 | Some(state) => state, 189 | None => return, 190 | }; 191 | let state = state.borrow(); 192 | let render_state = state.render_state.borrow(); 193 | let hl = &render_state.hl; 194 | 195 | // Draw the background first, to help GTK+ better notice that this doesn't change often 196 | let transparency = state.transparency(); 197 | snapshot_in.append_color( 198 | &hl.bg().to_rgbo(transparency.background_alpha), 199 | &Rect::new(0.0, 0.0, obj.width() as f32, obj.height() as f32), 200 | ); 201 | 202 | if state.nvim_clone().is_initialized() { 203 | // Render scenes get pretty huge here, so we cache them as often as possible 204 | let font_ctx = &render_state.font_ctx; 205 | if inner.snapshot_cache.is_none() { 206 | let ui_model = match state.grids.current_model() { 207 | Some(ui_model) => ui_model, 208 | None => return, 209 | }; 210 | 211 | inner.snapshot_cache = snapshot_nvim(font_ctx, ui_model, hl); 212 | } 213 | if let Some(ref cached_snapshot) = inner.snapshot_cache { 214 | let push_opacity = transparency.filled_alpha < 0.99999; 215 | if push_opacity { 216 | snapshot_in.push_opacity(transparency.filled_alpha) 217 | } 218 | 219 | snapshot_in.append_node(cached_snapshot); 220 | 221 | if push_opacity { 222 | snapshot_in.pop(); 223 | } 224 | } 225 | 226 | if let Some(cursor) = state.cursor() { 227 | if let Some(model) = state.grids.current_model() { 228 | snapshot_cursor(snapshot_in, cursor, font_ctx, model, hl, transparency); 229 | } 230 | } 231 | } else { 232 | self.snapshot_initializing(snapshot_in, &render_state); 233 | } 234 | } 235 | } 236 | 237 | impl NvimViewportObject { 238 | fn snapshot_initializing(&self, snapshot: >k::Snapshot, render_state: &RenderState) { 239 | let obj = self.obj(); 240 | let layout = obj.create_pango_layout(Some("Loading…")); 241 | 242 | let attr_list = pango::AttrList::new(); 243 | attr_list.insert(render_state.hl.fg().to_pango_fg()); 244 | layout.set_attributes(Some(&attr_list)); 245 | 246 | let (width, height) = layout.pixel_size(); 247 | snapshot.render_layout( 248 | &obj.style_context(), 249 | obj.allocated_width() as f64 / 2.0 - width as f64 / 2.0, 250 | obj.allocated_height() as f64 / 2.0 - height as f64 / 2.0, 251 | &layout, 252 | ); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/plug_manager/manager.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use super::store::{PlugInfo, Store}; 4 | use super::vim_plug; 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 manager; 2 | mod plugin_settings_dlg; 3 | mod store; 4 | mod ui; 5 | mod vim_plug; 6 | mod vimawesome; 7 | 8 | pub use self::manager::{Manager, PlugManagerConfigSource}; 9 | pub use self::ui::Ui; 10 | -------------------------------------------------------------------------------- /src/plug_manager/plugin_settings_dlg.rs: -------------------------------------------------------------------------------- 1 | use gtk::prelude::*; 2 | 3 | use super::store; 4 | 5 | pub struct Builder<'a> { 6 | title: &'a str, 7 | } 8 | 9 | impl<'a> Builder<'a> { 10 | pub fn new(title: &'a str) -> Self { 11 | Builder { title } 12 | } 13 | 14 | pub async fn show>(&self, parent: &F) -> Option { 15 | let dlg = gtk::Dialog::with_buttons( 16 | Some(self.title), 17 | Some(parent), 18 | gtk::DialogFlags::USE_HEADER_BAR | gtk::DialogFlags::DESTROY_WITH_PARENT, 19 | &[ 20 | ("Cancel", gtk::ResponseType::Cancel), 21 | ("Ok", gtk::ResponseType::Ok), 22 | ], 23 | ); 24 | 25 | let content = dlg.content_area(); 26 | let border = gtk::Box::builder() 27 | .orientation(gtk::Orientation::Horizontal) 28 | .margin_start(12) 29 | .margin_end(12) 30 | .margin_top(12) 31 | .margin_bottom(12) 32 | .build(); 33 | 34 | let list = gtk::ListBox::builder() 35 | .selection_mode(gtk::SelectionMode::None) 36 | .build(); 37 | 38 | let path = gtk::Box::builder() 39 | .orientation(gtk::Orientation::Horizontal) 40 | .spacing(5) 41 | .margin_start(5) 42 | .margin_bottom(5) 43 | .margin_top(5) 44 | .margin_end(5) 45 | .build(); 46 | let path_lbl = gtk::Label::new(Some("Repo")); 47 | let path_e = gtk::Entry::new(); 48 | path_e.set_placeholder_text(Some("user_name/repo_name")); 49 | 50 | path.append(&path_lbl); 51 | path.append(&path_e); 52 | 53 | list.append(&path); 54 | 55 | let name = gtk::Box::builder() 56 | .orientation(gtk::Orientation::Horizontal) 57 | .spacing(5) 58 | .margin_start(5) 59 | .margin_end(5) 60 | .margin_top(5) 61 | .margin_bottom(5) 62 | .build(); 63 | let name_lbl = gtk::Label::new(Some("Name")); 64 | let name_e = gtk::Entry::new(); 65 | 66 | name.append(&name_lbl); 67 | name.append(&name_e); 68 | 69 | list.append(&name); 70 | 71 | border.append(&list); 72 | content.append(&border); 73 | 74 | path_e.connect_changed(glib::clone!( 75 | #[strong] 76 | name_e, 77 | move |p| { 78 | if let Some(name) = extract_name(p.text().as_str()) { 79 | name_e.set_text(&name); 80 | } 81 | } 82 | )); 83 | 84 | let res = if dlg.run_future().await == gtk::ResponseType::Ok { 85 | let path = path_e.text().to_string(); 86 | let name = name_e.text(); 87 | 88 | let name = if name.trim().is_empty() { 89 | match extract_name(&path) { 90 | Some(name) => name, 91 | None => path.clone(), 92 | } 93 | } else { 94 | name.to_string() 95 | }; 96 | 97 | Some(store::PlugInfo::new(name, path)) 98 | } else { 99 | None 100 | }; 101 | 102 | dlg.close(); 103 | 104 | res 105 | } 106 | } 107 | 108 | fn extract_name(path: &str) -> Option { 109 | if let Some(idx) = path.rfind(['/', '\\']) { 110 | if idx < path.len() - 1 { 111 | let path = path.trim_end_matches(".git"); 112 | Some(path[idx + 1..].to_owned()) 113 | } else { 114 | None 115 | } 116 | } else { 117 | None 118 | } 119 | } 120 | 121 | #[cfg(test)] 122 | mod tests { 123 | use super::*; 124 | 125 | #[test] 126 | fn test_extract_name() { 127 | assert_eq!( 128 | Some("plugin_name".to_owned()), 129 | extract_name("http://github.com/somebody/plugin_name.git") 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/plug_manager/store.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use log::error; 4 | 5 | use super::vim_plug; 6 | use crate::settings::SettingsLoader; 7 | 8 | #[derive(Default)] 9 | pub struct Store { 10 | settings: Settings, 11 | } 12 | 13 | impl Store { 14 | pub fn is_config_exists() -> bool { 15 | Settings::is_file_exists() 16 | } 17 | 18 | pub fn is_enabled(&self) -> bool { 19 | self.settings.enabled 20 | } 21 | 22 | pub fn load() -> Self { 23 | Store { 24 | settings: Settings::load(), 25 | } 26 | } 27 | 28 | pub fn load_from_plug(vim_plug: &vim_plug::Manager) -> Self { 29 | let settings = match vim_plug.get_plugs() { 30 | Err(msg) => { 31 | error!("{}", msg); 32 | Default::default() 33 | } 34 | Ok(plugs) => { 35 | let plugs = plugs 36 | .iter() 37 | .map(|vpi| PlugInfo::new(vpi.name.to_owned(), vpi.uri.to_owned())) 38 | .collect(); 39 | Settings::new(plugs) 40 | } 41 | }; 42 | 43 | Store { settings } 44 | } 45 | 46 | pub fn get_plugs(&self) -> &[PlugInfo] { 47 | &self.settings.plugs 48 | } 49 | 50 | pub fn set_enabled(&mut self, enabled: bool) { 51 | self.settings.enabled = enabled; 52 | } 53 | 54 | pub fn clear_removed(&mut self) { 55 | self.settings.plugs.retain(|p| !p.removed); 56 | } 57 | 58 | pub fn save(&self) { 59 | self.settings.save(); 60 | } 61 | 62 | pub fn remove_plug(&mut self, idx: usize) { 63 | self.settings.plugs[idx].removed = true; 64 | } 65 | 66 | pub fn restore_plug(&mut self, idx: usize) { 67 | self.settings.plugs[idx].removed = false; 68 | } 69 | 70 | pub fn add_plug(&mut self, plug: PlugInfo) -> bool { 71 | let path = plug.get_plug_path(); 72 | if self 73 | .settings 74 | .plugs 75 | .iter() 76 | .any(|p| p.get_plug_path() == path || p.name == plug.name) 77 | { 78 | return false; 79 | } 80 | self.settings.plugs.push(plug); 81 | true 82 | } 83 | 84 | pub fn plugs_count(&self) -> usize { 85 | self.settings.plugs.len() 86 | } 87 | 88 | pub fn move_item(&mut self, idx: usize, offset: i32) { 89 | let plug = self.settings.plugs.remove(idx); 90 | self.settings 91 | .plugs 92 | .insert((idx as i32 + offset) as usize, plug); 93 | } 94 | } 95 | 96 | #[derive(Serialize, Deserialize, Default)] 97 | struct Settings { 98 | enabled: bool, 99 | plugs: Vec, 100 | } 101 | 102 | impl Settings { 103 | fn new(plugs: Vec) -> Self { 104 | Settings { 105 | plugs, 106 | enabled: false, 107 | } 108 | } 109 | } 110 | 111 | impl SettingsLoader for Settings { 112 | const SETTINGS_FILE: &'static str = "plugs.toml"; 113 | 114 | fn from_str(s: &str) -> Result { 115 | toml::from_str(s).map_err(|e| format!("{e}")) 116 | } 117 | } 118 | 119 | #[derive(Serialize, Deserialize)] 120 | pub struct PlugInfo { 121 | pub name: String, 122 | pub url: String, 123 | pub removed: bool, 124 | } 125 | 126 | impl PlugInfo { 127 | pub fn new(name: String, url: String) -> Self { 128 | PlugInfo { 129 | name, 130 | url, 131 | removed: false, 132 | } 133 | } 134 | 135 | pub fn get_plug_path(&self) -> String { 136 | if self.url.contains("github.com") { 137 | let mut path_comps: Vec<&str> = self 138 | .url 139 | .trim_end_matches(".git") 140 | .rsplit('/') 141 | .take(2) 142 | .collect(); 143 | path_comps.reverse(); 144 | path_comps.join("/") 145 | } else { 146 | self.url.clone() 147 | } 148 | } 149 | } 150 | 151 | #[cfg(test)] 152 | mod tests { 153 | use super::*; 154 | 155 | #[test] 156 | fn test_get_plug_path() { 157 | let plug = PlugInfo::new( 158 | "rust.vim".to_owned(), 159 | "https://git::@github.com/rust-lang/rust.vim.git".to_owned(), 160 | ); 161 | assert_eq!("rust-lang/rust.vim".to_owned(), plug.get_plug_path()); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/plug_manager/vim_plug.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use crate::nvim::{ErrorReport, NeovimClient, NvimSession}; 4 | use crate::spawn_timeout; 5 | use crate::value::ValueMapExt; 6 | 7 | pub struct Manager { 8 | nvim: Option>, 9 | } 10 | 11 | impl Manager { 12 | pub fn new() -> Self { 13 | Manager { nvim: None } 14 | } 15 | 16 | pub fn initialize(&mut self, nvim: Rc) { 17 | self.nvim = Some(nvim); 18 | } 19 | 20 | fn nvim(&self) -> Option { 21 | self.nvim.as_ref().unwrap().nvim() 22 | } 23 | 24 | pub fn get_plugs(&self) -> Result, String> { 25 | if let Some(nvim) = self.nvim() { 26 | let g_plugs = nvim 27 | .block_timeout(nvim.eval("g:plugs")) 28 | .map_err(|e| format!("Can't retrieve g:plugs map: {e}"))?; 29 | 30 | let plugs_map = g_plugs 31 | .as_map() 32 | .ok_or_else(|| "Can't retrieve g:plugs map".to_owned())? 33 | .to_attrs_map()?; 34 | 35 | let g_plugs_order = nvim 36 | .block_timeout(nvim.eval("g:plugs_order")) 37 | .map_err(|e| format!("{e}"))?; 38 | 39 | let order_arr = g_plugs_order 40 | .as_array() 41 | .ok_or_else(|| "Can't find g:plugs_order array".to_owned())?; 42 | 43 | let plugs_info: Vec = order_arr 44 | .iter() 45 | .map(|n| n.as_str()) 46 | .filter_map(|name| { 47 | if let Some(name) = name { 48 | plugs_map 49 | .get(name) 50 | .and_then(|desc| desc.as_map()) 51 | .and_then(|desc| desc.to_attrs_map().ok()) 52 | .and_then(|desc| { 53 | let uri = desc.get("uri").and_then(|uri| uri.as_str()); 54 | uri.map(|uri| VimPlugInfo::new(name.to_owned(), uri.to_owned())) 55 | }) 56 | } else { 57 | None 58 | } 59 | }) 60 | .collect(); 61 | Ok(plugs_info.into_boxed_slice()) 62 | } else { 63 | Err("Nvim not initialized".to_owned()) 64 | } 65 | } 66 | 67 | pub fn is_loaded(&self) -> bool { 68 | if let Some(nvim) = self.nvim() { 69 | let loaded_plug = nvim.block_timeout(nvim.eval("exists('g:loaded_plug')")); 70 | loaded_plug 71 | .ok_and_report() 72 | .and_then(|loaded_plug| loaded_plug.as_i64()) 73 | .is_some_and(|loaded_plug| loaded_plug > 0) 74 | } else { 75 | false 76 | } 77 | } 78 | 79 | pub fn reload(&self, path: &str) { 80 | let path = path.to_owned(); 81 | if let Some(nvim) = self.nvim() { 82 | spawn_timeout!(nvim.command(&format!("source {path}"))); 83 | } 84 | } 85 | } 86 | 87 | #[derive(Debug)] 88 | pub struct VimPlugInfo { 89 | pub name: String, 90 | pub uri: String, 91 | } 92 | 93 | impl VimPlugInfo { 94 | pub fn new(name: String, uri: String) -> Self { 95 | VimPlugInfo { name, uri } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /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::Deserialize; 7 | 8 | use gtk::prelude::*; 9 | 10 | use super::store::PlugInfo; 11 | 12 | pub fn call(query: Option, cb: F) 13 | where 14 | F: FnOnce(io::Result) + Send + 'static, 15 | { 16 | thread::spawn(move || { 17 | let mut result = Some(request(query.as_ref().map(|s| s.as_ref()))); 18 | let mut cb = Some(cb); 19 | 20 | glib::idle_add_once(move || cb.take().unwrap()(result.take().unwrap())) 21 | }); 22 | } 23 | 24 | fn request(query: Option<&str>) -> io::Result { 25 | let child = Command::new("curl") 26 | .arg("-s") 27 | .arg(format!( 28 | "https://vimawesome.com/api/plugins?query={}&page=1", 29 | query.unwrap_or("") 30 | )) 31 | .stdout(Stdio::piped()) 32 | .spawn()?; 33 | 34 | let out = child.wait_with_output()?; 35 | 36 | if out.status.success() { 37 | if out.stdout.is_empty() { 38 | Ok(DescriptionList::empty()) 39 | } else { 40 | let description_list: DescriptionList = serde_json::from_slice(&out.stdout) 41 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 42 | Ok(description_list) 43 | } 44 | } else { 45 | Err(io::Error::new( 46 | io::ErrorKind::Other, 47 | format!( 48 | "curl exit with error:\n{}", 49 | match out.status.code() { 50 | Some(code) => format!("Exited with status code: {code}"), 51 | None => "Process terminated by signal".to_owned(), 52 | } 53 | ), 54 | )) 55 | } 56 | } 57 | 58 | pub fn build_result_panel( 59 | list: &DescriptionList, 60 | add_cb: F, 61 | ) -> gtk::ScrolledWindow { 62 | let panel = gtk::ListBox::new(); 63 | let scroll = gtk::ScrolledWindow::builder() 64 | .child(&panel) 65 | .vexpand(true) 66 | .build(); 67 | 68 | let cb_ref = Rc::new(add_cb); 69 | for plug in list.plugins.iter() { 70 | let row = create_plug_row(plug, cb_ref.clone()); 71 | 72 | panel.append(&row); 73 | } 74 | 75 | scroll.show(); 76 | scroll 77 | } 78 | 79 | fn create_plug_row( 80 | plug: &Description, 81 | add_cb: Rc, 82 | ) -> gtk::ListBoxRow { 83 | let row_container = gtk::Box::builder() 84 | .orientation(gtk::Orientation::Vertical) 85 | .spacing(5) 86 | .margin_start(5) 87 | .margin_bottom(5) 88 | .margin_top(5) 89 | .margin_end(5) 90 | .build(); 91 | 92 | #[rustfmt::skip] 93 | let row = gtk::ListBoxRow::builder() 94 | .child(&row_container) 95 | .build(); 96 | 97 | let hbox = gtk::Box::new(gtk::Orientation::Horizontal, 5); 98 | let label_box = create_plug_label(plug); 99 | 100 | let button_box = gtk::Box::new(gtk::Orientation::Horizontal, 0); 101 | button_box.set_halign(gtk::Align::End); 102 | 103 | let add_btn = gtk::Button::with_label("Install"); 104 | button_box.append(&add_btn); 105 | 106 | row_container.append(&hbox); 107 | hbox.append(&label_box); 108 | hbox.append(&button_box); 109 | 110 | add_btn.connect_clicked(glib::clone!( 111 | #[strong] 112 | plug, 113 | move |btn| { 114 | if let Some(ref github_url) = plug.github_url { 115 | btn.set_sensitive(false); 116 | add_cb(PlugInfo::new(plug.name.clone(), github_url.clone())); 117 | } 118 | } 119 | )); 120 | 121 | row 122 | } 123 | 124 | fn create_plug_label(plug: &Description) -> gtk::Box { 125 | let label_box = gtk::Box::new(gtk::Orientation::Vertical, 5); 126 | 127 | let name_lbl = gtk::Label::new(None); 128 | name_lbl.set_markup(&format!( 129 | "{} by {}", 130 | plug.name, 131 | plug.author 132 | .as_ref() 133 | .map(|s| s.as_ref()) 134 | .unwrap_or("unknown",) 135 | )); 136 | name_lbl.set_halign(gtk::Align::Start); 137 | let url_lbl = gtk::Label::new(None); 138 | if let Some(url) = plug.github_url.as_ref() { 139 | url_lbl.set_markup(&format!("{url}")); 140 | } 141 | url_lbl.set_halign(gtk::Align::Start); 142 | 143 | label_box.append(&name_lbl); 144 | label_box.append(&url_lbl); 145 | label_box 146 | } 147 | 148 | #[derive(Deserialize, Debug)] 149 | pub struct DescriptionList { 150 | pub plugins: Box<[Description]>, 151 | } 152 | 153 | impl DescriptionList { 154 | fn empty() -> DescriptionList { 155 | DescriptionList { 156 | plugins: Box::new([]), 157 | } 158 | } 159 | } 160 | 161 | #[derive(Deserialize, Debug, Clone)] 162 | pub struct Description { 163 | pub name: String, 164 | pub github_url: Option, 165 | pub author: Option, 166 | } 167 | -------------------------------------------------------------------------------- /src/popup_menu/list_row.rs: -------------------------------------------------------------------------------- 1 | use super::popupmenu_model::PopupMenuItemRef; 2 | 3 | use std::{cell::RefCell, convert::*, rc::*}; 4 | 5 | use once_cell::sync::Lazy; 6 | 7 | use gtk::{prelude::*, subclass::prelude::*}; 8 | 9 | pub const PADDING: i32 = 2; 10 | 11 | glib::wrapper! { 12 | pub struct PopupMenuListRow(ObjectSubclass) 13 | @extends gtk::Box, gtk::Widget, 14 | @implements gtk::Accessible; 15 | } 16 | 17 | impl PopupMenuListRow { 18 | pub fn new(state: &Rc>) -> Self { 19 | glib::Object::builder::() 20 | .property("state", glib::BoxedAnyObject::new(state.clone())) 21 | .build() 22 | } 23 | 24 | pub fn set_row(&self, row: Option<&PopupMenuItemRef>) { 25 | self.set_property("row", row.cloned().map(glib::BoxedAnyObject::new)); 26 | } 27 | } 28 | 29 | #[derive(Default)] 30 | pub struct PopupMenuListRowObject { 31 | state: RefCell>>, 32 | word_label: glib::WeakRef, 33 | kind_label: glib::WeakRef, 34 | menu_label: glib::WeakRef, 35 | } 36 | 37 | #[glib::object_subclass] 38 | impl ObjectSubclass for PopupMenuListRowObject { 39 | const NAME: &'static str = "NvimPopupMenuListRow"; 40 | type Type = PopupMenuListRow; 41 | type ParentType = gtk::Box; 42 | } 43 | 44 | impl ObjectImpl for PopupMenuListRowObject { 45 | fn constructed(&self) { 46 | self.parent_constructed(); 47 | let obj = self.obj(); 48 | 49 | let word_label = gtk::Label::builder() 50 | .single_line_mode(true) 51 | .ellipsize(pango::EllipsizeMode::Middle) 52 | .xalign(0.0) 53 | .build(); 54 | self.word_label.set(Some(&word_label)); 55 | obj.append(&word_label); 56 | 57 | let kind_label = gtk::Label::builder() 58 | .visible(false) 59 | .single_line_mode(true) 60 | .ellipsize(pango::EllipsizeMode::End) 61 | .xalign(0.0) 62 | .build(); 63 | self.kind_label.set(Some(&kind_label)); 64 | obj.append(&kind_label); 65 | 66 | let menu_label = gtk::Label::builder() 67 | .visible(false) 68 | .single_line_mode(true) 69 | .ellipsize(pango::EllipsizeMode::Middle) 70 | .xalign(0.0) 71 | .build(); 72 | self.menu_label.set(Some(&menu_label)); 73 | obj.append(&menu_label); 74 | } 75 | 76 | fn properties() -> &'static [glib::ParamSpec] { 77 | static PROPERTIES: Lazy> = Lazy::new(|| { 78 | vec![ 79 | glib::ParamSpecObject::builder::("state") 80 | .write_only() 81 | .build(), 82 | glib::ParamSpecObject::builder::("row") 83 | .write_only() 84 | .build(), 85 | ] 86 | }); 87 | 88 | PROPERTIES.as_ref() 89 | } 90 | 91 | fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 92 | match pspec.name() { 93 | "row" => { 94 | let row = value.get_owned::>().unwrap(); 95 | 96 | if let Some(row) = row { 97 | let row = row.borrow::(); 98 | let state = self.state.borrow(); 99 | let state = state.borrow(); 100 | let word_label = self.word_label.upgrade().unwrap(); 101 | word_label.set_label(&row.word); 102 | word_label.set_width_request(state.word_col_width); 103 | 104 | let kind_label = self.kind_label.upgrade().unwrap(); 105 | kind_label.set_visible(state.kind_col_width.is_some()); 106 | kind_label.set_label(&row.kind); 107 | if let Some(width) = state.kind_col_width { 108 | kind_label.set_width_request(width); 109 | } 110 | 111 | let menu_label = self.menu_label.upgrade().unwrap(); 112 | menu_label.set_visible(state.menu_col_width.is_some()); 113 | menu_label.set_label(&row.menu); 114 | if let Some(width) = state.menu_col_width { 115 | menu_label.set_width_request(width); 116 | } 117 | } 118 | } 119 | "state" => { 120 | *self.state.borrow_mut() = value 121 | .get_owned::() 122 | .unwrap() 123 | .borrow::>>() 124 | .clone(); 125 | } 126 | _ => unreachable!(), 127 | } 128 | } 129 | } 130 | 131 | impl WidgetImpl for PopupMenuListRowObject {} 132 | impl BoxImpl for PopupMenuListRowObject {} 133 | 134 | /// A state struct that is shared across all PopupMenuListRow widgets. It is provided at 135 | /// construction 136 | #[derive(Default)] 137 | pub struct PopupMenuListRowState { 138 | pub word_col_width: i32, 139 | pub kind_col_width: Option, 140 | pub menu_col_width: Option, 141 | } 142 | -------------------------------------------------------------------------------- /src/popup_menu/popover.rs: -------------------------------------------------------------------------------- 1 | use std::convert::*; 2 | 3 | use once_cell::sync::*; 4 | 5 | use glib::SignalHandlerId; 6 | use gtk::{self, graphene::*, prelude::*, subclass::prelude::*}; 7 | 8 | glib::wrapper! { 9 | pub struct PopupMenuPopover(ObjectSubclass) 10 | @extends gtk::Popover, gtk::Native, gtk::Widget; 11 | } 12 | 13 | impl PopupMenuPopover { 14 | pub fn new() -> Self { 15 | glib::Object::new::() 16 | } 17 | 18 | pub fn connect_bounds_changed(&self, cb: F) -> SignalHandlerId 19 | where 20 | F: Fn(&Self, f32, f32, i32, i32) + 'static, 21 | { 22 | self.connect_local("bounds-change", true, move |values| { 23 | cb( 24 | values[0].get().as_ref().unwrap(), 25 | values[1].get().unwrap(), 26 | values[2].get().unwrap(), 27 | values[3].get().unwrap(), 28 | values[4].get().unwrap(), 29 | ); 30 | None 31 | }) 32 | } 33 | } 34 | 35 | #[derive(Default)] 36 | pub struct PopupMenuPopoverObject(()); 37 | 38 | #[glib::object_subclass] 39 | impl ObjectSubclass for PopupMenuPopoverObject { 40 | const NAME: &'static str = "NvimPopupMenuPopover"; 41 | type Type = PopupMenuPopover; 42 | type ParentType = gtk::Popover; 43 | } 44 | 45 | impl ObjectImpl for PopupMenuPopoverObject { 46 | fn signals() -> &'static [glib::subclass::Signal] { 47 | static SIGNALS: Lazy> = Lazy::new(|| { 48 | vec![glib::subclass::Signal::builder("bounds-change") 49 | .param_types([ 50 | glib::Type::F32, // x 51 | glib::Type::F32, // y 52 | glib::Type::I32, // w 53 | glib::Type::I32, // h 54 | ]) 55 | .build()] 56 | }); 57 | 58 | SIGNALS.as_ref() 59 | } 60 | } 61 | impl PopoverImpl for PopupMenuPopoverObject {} 62 | 63 | impl WidgetImpl for PopupMenuPopoverObject { 64 | fn size_allocate(&self, width: i32, height: i32, baseline: i32) { 65 | self.parent_size_allocate(width, height, baseline); 66 | 67 | let obj = self.obj(); 68 | let gdk_popup = obj.surface().unwrap().downcast::().unwrap(); 69 | 70 | let viewport = obj.parent().unwrap(); 71 | let root = obj.root().unwrap(); 72 | 73 | let viewport_bounds = viewport.compute_bounds(&root).unwrap(); 74 | 75 | obj.emit_by_name::<()>( 76 | "bounds-change", 77 | &[ 78 | &(gdk_popup.position_x() as f32 - viewport_bounds.x()), 79 | &(gdk_popup.position_y() as f32 - viewport_bounds.y()), 80 | &width, 81 | &height, 82 | ], 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/popup_menu/popupmenu_model.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | 3 | use gio::{prelude::*, subclass::prelude::*}; 4 | 5 | use std::{cell::RefCell, convert::*, ops::Deref, rc::Rc}; 6 | 7 | use crate::nvim::PopupMenuItem; 8 | 9 | glib::wrapper! { 10 | pub struct PopupMenuModel(ObjectSubclass) 11 | @implements gio::ListModel; 12 | } 13 | 14 | impl PopupMenuModel { 15 | pub fn new(items: &Rc>) -> Self { 16 | glib::Object::builder::() 17 | .property("items", glib::BoxedAnyObject::new(items.clone())) 18 | .build() 19 | } 20 | } 21 | 22 | #[derive(Default)] 23 | pub struct PopupMenuModelObject(RefCell>>); 24 | 25 | #[glib::object_subclass] 26 | impl ObjectSubclass for PopupMenuModelObject { 27 | const NAME: &'static str = "NvimPopupMenuModel"; 28 | type Type = PopupMenuModel; 29 | type Interfaces = (gio::ListModel,); 30 | } 31 | 32 | impl ObjectImpl for PopupMenuModelObject { 33 | fn properties() -> &'static [glib::ParamSpec] { 34 | static PROPERTIES: Lazy> = Lazy::new(|| { 35 | vec![ 36 | glib::ParamSpecObject::builder::("items") 37 | .write_only() 38 | .build(), 39 | ] 40 | }); 41 | 42 | PROPERTIES.as_ref() 43 | } 44 | 45 | fn set_property(&self, _id: usize, value: &glib::Value, pspec: &glib::ParamSpec) { 46 | match pspec.name() { 47 | "items" => { 48 | *self.0.borrow_mut() = value 49 | .get::() 50 | .unwrap() 51 | .borrow::>>() 52 | .clone() 53 | } 54 | _ => unreachable!(), 55 | } 56 | } 57 | } 58 | 59 | impl ListModelImpl for PopupMenuModelObject { 60 | fn item(&self, position: u32) -> Option { 61 | let items = self.0.borrow(); 62 | PopupMenuItemRef::new(&items, position as usize) 63 | .map(|c| glib::BoxedAnyObject::new(c).upcast()) 64 | } 65 | 66 | fn n_items(&self) -> u32 { 67 | self.0.borrow().len().try_into().unwrap() 68 | } 69 | 70 | fn item_type(&self) -> glib::Type { 71 | glib::BoxedAnyObject::static_type() 72 | } 73 | } 74 | 75 | #[derive(Clone, Default)] 76 | pub struct PopupMenuItemRef { 77 | array: Rc>, 78 | pos: usize, 79 | } 80 | 81 | impl PopupMenuItemRef { 82 | pub fn new(array: &Rc>, pos: usize) -> Option { 83 | array.get(pos).map(|_| Self { 84 | array: array.clone(), 85 | pos, 86 | }) 87 | } 88 | } 89 | 90 | impl Deref for PopupMenuItemRef { 91 | type Target = PopupMenuItem; 92 | 93 | fn deref(&self) -> &Self::Target { 94 | // SAFETY: pos is checked at creation time 95 | unsafe { self.array.get_unchecked(self.pos) } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/render/context.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use pango::{self, prelude::*}; 4 | 5 | use super::itemize::ItemizeIterator; 6 | use crate::ui_model::StyledLine; 7 | 8 | pub struct Context { 9 | font_metrics: FontMetrix, 10 | font_features: FontFeatures, 11 | line_space: i32, 12 | } 13 | 14 | impl Context { 15 | pub fn new(pango_context: pango::Context) -> Self { 16 | Context { 17 | line_space: 0, 18 | font_metrics: FontMetrix::new(pango_context, 0), 19 | font_features: FontFeatures::new(), 20 | } 21 | } 22 | 23 | pub fn update(&mut self, pango_context: pango::Context) { 24 | self.font_metrics = FontMetrix::new(pango_context, self.line_space); 25 | } 26 | 27 | pub fn update_font_features(&mut self, font_features: FontFeatures) { 28 | self.font_features = font_features; 29 | } 30 | 31 | pub fn update_line_space(&mut self, line_space: i32) { 32 | self.line_space = line_space; 33 | let pango_context = self.font_metrics.pango_context.clone(); 34 | self.font_metrics = FontMetrix::new(pango_context, self.line_space); 35 | } 36 | 37 | pub fn itemize(&self, line: &StyledLine) -> Vec { 38 | let attr_iter = line.attr_list.iterator(); 39 | 40 | ItemizeIterator::new(&line.line_str) 41 | .flat_map(|res| { 42 | let pango_context = &self.font_metrics.pango_context; 43 | let offset = res.offset as i32; 44 | let len = res.len as i32; 45 | 46 | let first_res = pango::itemize( 47 | pango_context, 48 | &line.line_str, 49 | offset, 50 | len, 51 | &line.attr_list, 52 | Some(&attr_iter), 53 | ); 54 | 55 | if !res.avoid_break || first_res.len() == 1 { 56 | return first_res; 57 | } 58 | 59 | /* If we get multiple items, and it isn't from a multi-character ASCII string, then 60 | * it's likely from an additional split pango had to perform because not all chars 61 | * in the string were available in the current font. When this happens, in order to 62 | * ensure combining characters are rendered correctly we need to try reitemizing the 63 | * whole thing with the font containing the missing glyphs. Failing that, we 64 | * fallback to the original (likely incorrect) itemization result. 65 | */ 66 | let our_font = self.font_description(); 67 | let extra_fonts = first_res.iter().filter_map(|i| { 68 | let font = i.analysis().font().describe(); 69 | if font != *our_font { 70 | Some(font) 71 | } else { 72 | None 73 | } 74 | }); 75 | 76 | // We do res.len() - 2 so that in the likely event that most of the Cell rendered 77 | // with our_font, and the rest with another, we're able to skip allocating the 78 | // HashSet completely. 79 | let mut seen = HashSet::with_capacity(first_res.len() - 2); 80 | let mut new_res = None; 81 | for font_desc in extra_fonts { 82 | if seen.contains(&font_desc) { 83 | continue; 84 | } 85 | 86 | pango_context.set_font_description(Some(&font_desc)); 87 | let res = pango::itemize( 88 | pango_context, 89 | &line.line_str, 90 | offset, 91 | len, 92 | &line.attr_list, 93 | None, 94 | ); 95 | 96 | let len = res.len(); 97 | if len == 1 || len < new_res.as_ref().unwrap_or(&first_res).len() { 98 | new_res = Some(res); 99 | if len == 1 { 100 | break; 101 | } 102 | } 103 | seen.insert(font_desc); 104 | } 105 | 106 | pango_context.set_font_description(Some(our_font)); 107 | new_res.unwrap_or(first_res) 108 | }) 109 | .collect() 110 | } 111 | 112 | pub fn create_layout(&self) -> pango::Layout { 113 | pango::Layout::new(&self.font_metrics.pango_context) 114 | } 115 | 116 | pub fn font_description(&self) -> &pango::FontDescription { 117 | &self.font_metrics.font_desc 118 | } 119 | 120 | pub fn cell_metrics(&self) -> &CellMetrics { 121 | &self.font_metrics.cell_metrics 122 | } 123 | 124 | pub fn font_features(&self) -> &FontFeatures { 125 | &self.font_features 126 | } 127 | 128 | pub fn font_families(&self) -> HashSet { 129 | self.font_metrics 130 | .pango_context 131 | .list_families() 132 | .iter() 133 | .map(|f| f.name()) 134 | .collect() 135 | } 136 | } 137 | 138 | struct FontMetrix { 139 | pango_context: pango::Context, 140 | cell_metrics: CellMetrics, 141 | font_desc: pango::FontDescription, 142 | } 143 | 144 | impl FontMetrix { 145 | pub fn new(pango_context: pango::Context, line_space: i32) -> Self { 146 | let font_metrics = 147 | pango_context.metrics(None, Some(&pango::Language::from_string("en_US"))); 148 | let font_desc = pango_context.font_description().unwrap(); 149 | 150 | FontMetrix { 151 | pango_context, 152 | cell_metrics: CellMetrics::new(&font_metrics, line_space), 153 | font_desc, 154 | } 155 | } 156 | } 157 | 158 | // TODO: See if we can convert most of these to f32 159 | pub struct CellMetrics { 160 | pub line_height: f64, 161 | pub char_width: f64, 162 | pub ascent: f64, 163 | pub descent: f64, 164 | pub underline_position: f64, 165 | pub underline_thickness: f64, 166 | pub strikethrough_position: f64, 167 | pub strikethrough_thickness: f64, 168 | pub pango_ascent: i32, 169 | pub pango_descent: i32, 170 | pub pango_char_width: i32, 171 | } 172 | 173 | impl CellMetrics { 174 | fn new(font_metrics: &pango::FontMetrics, line_space: i32) -> Self { 175 | let ascent = (f64::from(font_metrics.ascent()) / f64::from(pango::SCALE)).ceil(); 176 | let descent = (f64::from(font_metrics.descent()) / f64::from(pango::SCALE)).ceil(); 177 | 178 | // distance above top of underline, will typically be negative 179 | let pango_underline_position = f64::from(font_metrics.underline_position()); 180 | let underline_position = (pango_underline_position / f64::from(pango::SCALE)) 181 | .abs() 182 | .ceil() 183 | .copysign(pango_underline_position); 184 | 185 | let underline_thickness = 186 | (f64::from(font_metrics.underline_thickness()) / f64::from(pango::SCALE)).ceil(); 187 | 188 | let strikethrough_position = 189 | (f64::from(font_metrics.strikethrough_position()) / f64::from(pango::SCALE)).ceil(); 190 | let strikethrough_thickness = 191 | (f64::from(font_metrics.strikethrough_thickness()) / f64::from(pango::SCALE)).ceil(); 192 | 193 | CellMetrics { 194 | pango_ascent: font_metrics.ascent(), 195 | pango_descent: font_metrics.descent(), 196 | pango_char_width: font_metrics.approximate_char_width(), 197 | ascent, 198 | descent, 199 | line_height: ascent + descent + f64::from(line_space), 200 | char_width: f64::from(font_metrics.approximate_char_width()) / f64::from(pango::SCALE), 201 | underline_position: ascent - underline_position + underline_thickness / 2.0, 202 | underline_thickness, 203 | strikethrough_position: ascent - strikethrough_position + strikethrough_thickness / 2.0, 204 | strikethrough_thickness, 205 | } 206 | } 207 | 208 | #[cfg(test)] 209 | pub fn new_hw(line_height: f64, char_width: f64) -> Self { 210 | CellMetrics { 211 | pango_ascent: 0, 212 | pango_descent: 0, 213 | pango_char_width: 0, 214 | ascent: 0.0, 215 | descent: 0.0, 216 | line_height, 217 | char_width, 218 | underline_position: 0.0, 219 | underline_thickness: 0.0, 220 | strikethrough_position: 0.0, 221 | strikethrough_thickness: 0.0, 222 | } 223 | } 224 | 225 | // Translate the given grid coordinates into their actual pixel coordinates 226 | pub fn get_pixel_coords(&self, (row, col): (usize, usize)) -> (f64, f64) { 227 | (self.char_width * col as f64, self.line_height * row as f64) 228 | } 229 | 230 | /* Translate the given pixel coordinates to their positions on the grid, while allowing for 231 | * fractional values to be returned (nvim asks for this sometimes!) 232 | */ 233 | pub fn get_fractional_grid_area( 234 | &self, 235 | (x, y, w, h): (f64, f64, f64, f64), 236 | ) -> (f64, f64, f64, f64) { 237 | ( 238 | x / self.char_width, 239 | y / self.line_height, 240 | w / self.char_width, 241 | h / self.line_height, 242 | ) 243 | } 244 | 245 | // Convert a count of cells to its respective length in pixels 246 | pub fn get_cell_len(&self, len: usize) -> f64 { 247 | self.char_width * len as f64 248 | } 249 | } 250 | 251 | pub struct FontFeatures { 252 | attr: Option, 253 | } 254 | 255 | impl FontFeatures { 256 | pub fn new() -> Self { 257 | FontFeatures { attr: None } 258 | } 259 | 260 | pub fn from(font_features: String) -> Self { 261 | if font_features.trim().is_empty() { 262 | return Self::new(); 263 | } 264 | 265 | FontFeatures { 266 | attr: Some(pango::AttrFontFeatures::new(&font_features).upcast()), 267 | } 268 | } 269 | 270 | pub fn insert_into(&self, attr_list: &pango::AttrList) { 271 | if let Some(ref attr) = self.attr { 272 | attr_list.insert(attr.clone()); 273 | } 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/render/itemize.rs: -------------------------------------------------------------------------------- 1 | use unicode_segmentation::*; 2 | 3 | #[derive(Debug, PartialEq, Eq)] 4 | pub struct ItemizeResult { 5 | pub offset: usize, 6 | pub len: usize, 7 | pub avoid_break: bool, 8 | } 9 | 10 | impl ItemizeResult { 11 | pub fn new(offset: usize, len: usize, avoid_break: bool) -> Self { 12 | Self { 13 | offset, 14 | len, 15 | avoid_break, 16 | } 17 | } 18 | } 19 | 20 | pub struct ItemizeIterator<'a> { 21 | grapheme_iter: GraphemeIndices<'a>, 22 | line: &'a str, 23 | prev_grapheme: Option<(usize, &'a str)>, 24 | } 25 | 26 | impl<'a> ItemizeIterator<'a> { 27 | pub fn new(line: &'a str) -> Self { 28 | ItemizeIterator { 29 | grapheme_iter: line.grapheme_indices(true), 30 | line, 31 | prev_grapheme: None, 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * Iterates through a line of text while itemizing it into the largest possible clusters of 38 | * non-whitespace characters that can be drawn at once without risking column misalignment from 39 | * ambiguous width characters. This means for ASCII where the size of non-whitespace is essentially 40 | * guaranteed to be consistent, items will ideally be per-word to speed up rendering. For Unicode, 41 | * items will be per-grapheme to ensure correct monospaced display. 42 | */ 43 | impl Iterator for ItemizeIterator<'_> { 44 | type Item = ItemizeResult; 45 | 46 | fn next(&mut self) -> Option { 47 | let mut start_index = None; 48 | let mut avoid_break = false; 49 | 50 | let end_index = loop { 51 | let grapheme_indice = self 52 | .prev_grapheme 53 | .take() 54 | .or_else(|| self.grapheme_iter.next()); 55 | if let Some((index, grapheme)) = grapheme_indice { 56 | // Figure out if this grapheme is whitespace and/or ASCII in one iteration 57 | let mut is_whitespace = true; 58 | let mut is_ascii = true; 59 | for c in grapheme.chars() { 60 | if is_whitespace { 61 | if c.is_whitespace() { 62 | continue; 63 | } 64 | is_whitespace = false; 65 | } 66 | if !c.is_ascii() { 67 | is_ascii = false; 68 | break; 69 | } 70 | } 71 | 72 | if start_index.is_none() && !is_whitespace { 73 | start_index = Some(index); 74 | if !is_ascii { 75 | avoid_break = true; 76 | break index + grapheme.len(); 77 | } 78 | } 79 | if start_index.is_some() && (is_whitespace || !is_ascii) { 80 | self.prev_grapheme = grapheme_indice; 81 | break index; 82 | } 83 | } else { 84 | break self.line.len(); 85 | } 86 | }; 87 | 88 | start_index.map(|start_index| { 89 | ItemizeResult::new(start_index, end_index - start_index, avoid_break) 90 | }) 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | 98 | #[test] 99 | fn test_iterator() { 100 | let mut iter = ItemizeIterator::new("Test line 啊啊 ते "); 101 | 102 | assert_eq!(Some(ItemizeResult::new(0, 4, false)), iter.next()); 103 | assert_eq!(Some(ItemizeResult::new(6, 4, false)), iter.next()); 104 | assert_eq!(Some(ItemizeResult::new(11, 3, true)), iter.next()); 105 | assert_eq!(Some(ItemizeResult::new(14, 3, true)), iter.next()); 106 | assert_eq!(Some(ItemizeResult::new(18, 6, true)), iter.next()); 107 | assert_eq!(None, iter.next()); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::{Rc, Weak}; 3 | 4 | use log::error; 5 | 6 | use crate::shell::Shell; 7 | #[cfg(unix)] 8 | use gio::{self, prelude::*}; 9 | 10 | #[derive(PartialEq, Eq)] 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 { 37 | font_source: FontSource::Default, 38 | } 39 | } 40 | 41 | #[cfg(unix)] 42 | fn update_font(&mut self, shell: &mut Shell) { 43 | // rpc is priority for font 44 | if self.font_source == FontSource::Rpc { 45 | return; 46 | } 47 | 48 | shell.set_font_desc(&self.gnome_interface_settings.string("monospace-font-name")); 49 | self.font_source = FontSource::Gnome; 50 | } 51 | } 52 | 53 | pub struct Settings { 54 | shell: Option>>, 55 | state: Rc>, 56 | } 57 | 58 | impl Settings { 59 | pub fn new() -> Settings { 60 | Settings { 61 | shell: None, 62 | state: Rc::new(RefCell::new(State::new())), 63 | } 64 | } 65 | 66 | pub fn set_shell(&mut self, shell: Weak>) { 67 | self.shell = Some(shell); 68 | } 69 | 70 | #[cfg(unix)] 71 | pub fn init(&mut self) { 72 | let shell = Weak::upgrade(self.shell.as_ref().unwrap()).unwrap(); 73 | let state = self.state.clone(); 74 | self.state.borrow_mut().update_font(&mut shell.borrow_mut()); 75 | self.state 76 | .borrow() 77 | .gnome_interface_settings 78 | .connect_changed(None, move |_, _| { 79 | monospace_font_changed(&mut shell.borrow_mut(), &mut state.borrow_mut()) 80 | }); 81 | } 82 | 83 | #[cfg(target_os = "windows")] 84 | pub fn init(&mut self) {} 85 | 86 | pub fn set_font_source(&mut self, src: FontSource) { 87 | self.state.borrow_mut().font_source = src; 88 | } 89 | } 90 | 91 | #[cfg(unix)] 92 | fn monospace_font_changed(shell: &mut Shell, state: &mut State) { 93 | // rpc is priority for font 94 | if state.font_source != FontSource::Rpc { 95 | state.update_font(shell); 96 | } 97 | } 98 | 99 | use std::fs::File; 100 | use std::io::prelude::*; 101 | use std::path::Path; 102 | 103 | use crate::dirs; 104 | 105 | pub trait SettingsLoader: Sized + serde::Serialize + Default { 106 | const SETTINGS_FILE: &'static str; 107 | 108 | fn from_str(s: &str) -> Result; 109 | 110 | fn load() -> Self { 111 | match load_err() { 112 | Ok(settings) => settings, 113 | Err(e) => { 114 | error!("{}", e); 115 | Default::default() 116 | } 117 | } 118 | } 119 | 120 | fn is_file_exists() -> bool { 121 | dirs::app_config_dir() 122 | .to_path_buf() 123 | .join(Self::SETTINGS_FILE) 124 | .is_file() 125 | } 126 | 127 | fn save(&self) { 128 | match save_err(self) { 129 | Ok(()) => (), 130 | Err(e) => error!("{}", e), 131 | } 132 | } 133 | } 134 | 135 | fn load_from_file(path: &Path) -> Result { 136 | if path.exists() { 137 | let mut file = File::open(path).map_err(|e| format!("{e}"))?; 138 | let mut contents = String::new(); 139 | file.read_to_string(&mut contents) 140 | .map_err(|e| format!("{e}"))?; 141 | T::from_str(&contents) 142 | } else { 143 | Ok(Default::default()) 144 | } 145 | } 146 | 147 | fn load_err() -> Result { 148 | let mut toml_path = dirs::app_config_dir_create()?; 149 | toml_path.push(T::SETTINGS_FILE); 150 | load_from_file(&toml_path) 151 | } 152 | 153 | fn save_err(sl: &T) -> Result<(), String> { 154 | let mut toml_path = dirs::app_config_dir_create()?; 155 | toml_path.push(T::SETTINGS_FILE); 156 | let mut file = File::create(toml_path).map_err(|e| format!("{e}"))?; 157 | 158 | let contents = toml::to_string::(sl).map_err(|e| format!("{e}"))?; 159 | 160 | file.write_all(contents.as_bytes()) 161 | .map_err(|e| format!("{e}"))?; 162 | 163 | Ok(()) 164 | } 165 | -------------------------------------------------------------------------------- /src/shell_dlg.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, convert::*, rc::Rc, sync::Arc}; 2 | 3 | use log::{error, warn}; 4 | 5 | use gtk::prelude::*; 6 | use gtk::{ButtonsType, MessageDialog, MessageType}; 7 | 8 | use crate::nvim::{NeovimClient, NormalError, NvimSession, SessionError}; 9 | use crate::shell::Shell; 10 | use crate::ui::{Components, UiMutex}; 11 | use nvim_rs::Value; 12 | 13 | pub fn can_close_window( 14 | comps: &Arc>, 15 | shell: &Rc>, 16 | nvim: &Rc, 17 | ) -> bool { 18 | if comps.borrow().exit_confirmed { 19 | return true; 20 | } 21 | 22 | if let Some(ref nvim) = nvim.nvim() { 23 | if nvim.is_blocked() { 24 | return false; 25 | } 26 | 27 | match get_changed_buffers(nvim) { 28 | Ok(vec) => { 29 | if !vec.is_empty() { 30 | let comps = comps.clone(); 31 | let shell = shell.clone(); 32 | glib::MainContext::default().spawn_local(async move { 33 | let res = { 34 | let mut comps = comps.borrow_mut(); 35 | let res = show_not_saved_dlg(&comps, shell, &vec).await; 36 | 37 | comps.exit_confirmed = res; 38 | res 39 | }; 40 | 41 | if res { 42 | comps.borrow().close_window(); 43 | } 44 | }); 45 | false 46 | } else { 47 | true 48 | } 49 | } 50 | Err(ref err) => { 51 | error!("Error getting info from nvim: {}", err); 52 | true 53 | } 54 | } 55 | } else { 56 | true 57 | } 58 | } 59 | 60 | async fn show_not_saved_dlg( 61 | comps: &Components, 62 | shell: Rc>, 63 | changed_bufs: &[String], 64 | ) -> bool { 65 | let mut changed_files = changed_bufs 66 | .iter() 67 | .map(|n| if n.is_empty() { "" } else { n }) 68 | .fold(String::new(), |acc, v| acc + v + "\n"); 69 | changed_files.pop(); 70 | 71 | let flags = gtk::DialogFlags::MODAL | gtk::DialogFlags::DESTROY_WITH_PARENT; 72 | let dlg = MessageDialog::new( 73 | Some(comps.window()), 74 | flags, 75 | MessageType::Question, 76 | ButtonsType::None, 77 | format!("Save changes to '{changed_files}'?"), 78 | ); 79 | 80 | dlg.add_buttons(&[ 81 | ("_Yes", gtk::ResponseType::Yes), 82 | ("_No", gtk::ResponseType::No), 83 | ("_Cancel", gtk::ResponseType::Cancel), 84 | ]); 85 | 86 | let res = match dlg.run_future().await { 87 | gtk::ResponseType::Yes => { 88 | let nvim = shell.borrow().state.borrow().nvim().unwrap(); 89 | 90 | // FIXME: Figure out a way to use timeouts with nvim interactions when using glib for 91 | // async execution, either that or just don't use timeouts 92 | match nvim.command("wa").await { 93 | Err(err) => { 94 | match NormalError::try_from(&*err) { 95 | Ok(err) => err.print(&nvim).await, 96 | Err(_) => error!("Error: {}", err), 97 | }; 98 | false 99 | } 100 | _ => true, 101 | } 102 | } 103 | gtk::ResponseType::No => true, 104 | _ => false, 105 | }; 106 | 107 | dlg.close(); 108 | 109 | res 110 | } 111 | 112 | fn get_changed_buffers(nvim: &NvimSession) -> Result, SessionError> { 113 | let buffers = nvim.block_timeout(nvim.list_bufs()).unwrap(); 114 | 115 | Ok(buffers 116 | .iter() 117 | .map(|buf| { 118 | ( 119 | match nvim.block_timeout(buf.get_option("modified")) { 120 | Ok(Value::Boolean(val)) => val, 121 | Ok(_) => { 122 | warn!("Value must be boolean"); 123 | false 124 | } 125 | Err(ref err) => { 126 | error!("Something going wrong while getting buffer option: {}", err); 127 | false 128 | } 129 | }, 130 | match nvim.block_timeout(buf.get_name()) { 131 | Ok(name) => name, 132 | Err(ref err) => { 133 | error!("Something going wrong while getting buffer name: {}", err); 134 | "".to_owned() 135 | } 136 | }, 137 | ) 138 | }) 139 | .filter(|e| e.0) 140 | .map(|e| e.1) 141 | .collect()) 142 | } 143 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | /* CSS stylings for neovim-gtk that don't change */ 2 | popover.nvim-popover > contents { 3 | padding: 3px; 4 | } 5 | 6 | .nvim-background { 7 | background: rgba(0, 0, 0, 0); 8 | } 9 | 10 | listview.nvim-popupmenu-list { 11 | margin: 0px; 12 | padding: 0px; 13 | } 14 | 15 | /* vim: colorcolumn=100 tw=100 ts=4 sts=4 sw=4 expandtab : 16 | */ 17 | -------------------------------------------------------------------------------- /src/subscriptions.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use log::error; 4 | 5 | use nvim_rs::Value; 6 | 7 | use crate::{nvim::NvimSession, spawn_timeout}; 8 | 9 | /// A subscription to a Neovim autocmd event. 10 | struct Subscription { 11 | /// A callback to be executed each time the event triggers. 12 | cb: Box) + 'static>, 13 | /// A list of expressions which will be evaluated when the event triggers. The result is passed 14 | /// to the callback. 15 | args: Vec, 16 | } 17 | 18 | /// Subscription keys represent a NeoVim event coupled with a matching pattern. It is expected for 19 | /// the pattern more often than not to be `"*"`. 20 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 21 | pub struct SubscriptionKey { 22 | event_name: String, 23 | pattern: String, 24 | } 25 | 26 | impl<'a> From<&'a str> for SubscriptionKey { 27 | fn from(event_name: &'a str) -> Self { 28 | SubscriptionKey { 29 | event_name: event_name.to_owned(), 30 | pattern: "*".to_owned(), 31 | } 32 | } 33 | } 34 | 35 | impl SubscriptionKey { 36 | pub fn with_pattern(event_name: &str, pattern: &str) -> Self { 37 | SubscriptionKey { 38 | event_name: event_name.to_owned(), 39 | pattern: pattern.to_owned(), 40 | } 41 | } 42 | } 43 | 44 | /// A map of all registered subscriptions. 45 | pub struct Subscriptions(HashMap>); 46 | 47 | /// A handle to identify a `Subscription` within the `Subscriptions` map. 48 | /// 49 | /// Can be used to trigger the subscription manually even when the event was not triggered. 50 | /// 51 | /// Could be used in the future to suspend individual subscriptions. 52 | #[derive(Debug)] 53 | pub struct SubscriptionHandle { 54 | key: SubscriptionKey, 55 | index: usize, 56 | } 57 | 58 | impl Subscriptions { 59 | pub fn new() -> Self { 60 | Subscriptions(HashMap::new()) 61 | } 62 | 63 | /// Subscribe to a Neovim autocmd event. 64 | /// 65 | /// Subscriptions are not active immediately but only after `set_autocmds` is called. At the 66 | /// moment, all calls to `subscribe` must be made before calling `set_autocmds`. 67 | /// 68 | /// This function is wrapped by `shell::State`. 69 | /// 70 | /// # Arguments: 71 | /// 72 | /// - `key`: The subscription key to register. 73 | /// See `:help autocmd-events` for a list of supported event names. Event names can be 74 | /// comma-separated. 75 | /// 76 | /// - `args`: A list of expressions to be evaluated when the event triggers. 77 | /// Expressions are evaluated using Vimscript. The results are passed to the callback as a 78 | /// list of Strings. 79 | /// This is especially useful as `Neovim::eval` is synchronous and might block if called from 80 | /// the callback function; so always use the `args` mechanism instead. 81 | /// 82 | /// - `cb`: The callback function. 83 | /// This will be called each time the event triggers or when `run_now` is called. 84 | /// It is passed a vector with the results of the evaluated expressions given with `args`. 85 | /// 86 | /// # Example 87 | /// 88 | /// Call a function each time a buffer is entered or the current working directory is changed. 89 | /// Pass the current buffer name and directory to the callback. 90 | /// ``` 91 | /// let my_subscription = shell.state.borrow() 92 | /// .subscribe("BufEnter,DirChanged", &["expand(@%)", "getcwd()"], move |args| { 93 | /// let filename = &args[0]; 94 | /// let dir = &args[1]; 95 | /// // do stuff 96 | /// }); 97 | /// ``` 98 | pub fn subscribe(&mut self, key: SubscriptionKey, args: &[&str], cb: F) -> SubscriptionHandle 99 | where 100 | F: Fn(Vec) + 'static, 101 | { 102 | let entry = self.0.entry(key.clone()).or_default(); 103 | let index = entry.len(); 104 | entry.push(Subscription { 105 | cb: Box::new(cb), 106 | args: args.iter().map(|&s| s.to_owned()).collect(), 107 | }); 108 | SubscriptionHandle { key, index } 109 | } 110 | 111 | /// Register all subscriptions with Neovim. 112 | /// 113 | /// This function is wrapped by `shell::State`. 114 | pub fn set_autocmds(&self, nvim: &NvimSession) { 115 | for (key, subscriptions) in &self.0 { 116 | let SubscriptionKey { 117 | event_name, 118 | pattern, 119 | } = key; 120 | for (i, subscription) in subscriptions.iter().enumerate() { 121 | let args = subscription 122 | .args 123 | .iter() 124 | .fold("".to_owned(), |acc, arg| acc + ", " + arg); 125 | let autocmd = format!( 126 | "autocmd {event_name} {pattern} call rpcnotify(1, 'subscription', '{event_name}', '{pattern}', {i} {args})", 127 | ); 128 | spawn_timeout!(nvim.command(&autocmd)); 129 | } 130 | } 131 | } 132 | 133 | /// Trigger given event. 134 | fn on_notify(&self, key: &SubscriptionKey, index: usize, args: Vec) { 135 | if let Some(subscription) = self.0.get(key).and_then(|v| v.get(index)) { 136 | (*subscription.cb)(args); 137 | } 138 | } 139 | 140 | /// Wrapper around `on_notify` for easy calling with a `neovim_lib::Handler` implementation. 141 | /// 142 | /// This function is wrapped by `shell::State`. 143 | pub fn notify(&self, params: Vec) -> Result<(), String> { 144 | let mut params_iter = params.into_iter(); 145 | let ev_name = params_iter.next(); 146 | let ev_name = ev_name 147 | .as_ref() 148 | .and_then(Value::as_str) 149 | .ok_or("Error reading event name")?; 150 | let pattern = params_iter.next(); 151 | let pattern = pattern 152 | .as_ref() 153 | .and_then(Value::as_str) 154 | .ok_or("Error reading pattern")?; 155 | let key = SubscriptionKey { 156 | event_name: String::from(ev_name), 157 | pattern: String::from(pattern), 158 | }; 159 | let index = params_iter 160 | .next() 161 | .and_then(|i| i.as_u64()) 162 | .ok_or("Error reading index")? as usize; 163 | let args = params_iter 164 | .map(|arg| { 165 | arg.as_str() 166 | .map(str::to_owned) 167 | .or_else(|| arg.as_u64().map(|uint| uint.to_string())) 168 | }) 169 | .collect::>>() 170 | .ok_or("Error reading args")?; 171 | self.on_notify(&key, index, args); 172 | Ok(()) 173 | } 174 | 175 | /// Manually trigger the given subscription. 176 | /// 177 | /// The `nvim` instance is needed to evaluate the `args` expressions. 178 | /// 179 | /// This function is wrapped by `shell::State`. 180 | pub fn run_now(&self, handle: &SubscriptionHandle, nvim: &NvimSession) { 181 | let subscription = &self.0.get(&handle.key).unwrap()[handle.index]; 182 | let args = subscription 183 | .args 184 | .iter() 185 | .map(|arg| nvim.block_timeout(nvim.eval(arg))) 186 | .map(|res| { 187 | res.ok().and_then(|val| { 188 | val.as_str() 189 | .map(str::to_owned) 190 | .or_else(|| val.as_u64().map(|uint: u64| format!("{uint}"))) 191 | }) 192 | }) 193 | .collect::>>(); 194 | if let Some(args) = args { 195 | self.on_notify(&handle.key, handle.index, args); 196 | } else { 197 | error!("Error manually running {:?}", handle); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/tabline.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, collections::hash_map::HashMap, ops::Deref, rc::Rc}; 2 | 3 | use gtk::prelude::*; 4 | 5 | use crate::{ 6 | nvim::{self, ErrorReport, NvimSession, Tabpage}, 7 | spawn_timeout_user_err, 8 | }; 9 | 10 | struct State { 11 | tabpages: Vec, 12 | selected: Option, 13 | /// Since GTK only gives us the new index (at least afaict) of the tab page after a reorder 14 | /// operation, we keep a map of tab widgets to their last known position 15 | widget_map: HashMap, 16 | nvim: Option>, 17 | } 18 | 19 | impl State { 20 | pub fn new() -> Self { 21 | State { 22 | tabpages: Vec::new(), 23 | widget_map: HashMap::new(), 24 | selected: None, 25 | nvim: None, 26 | } 27 | } 28 | 29 | fn nvim(&self) -> NvimSession { 30 | self.nvim 31 | .as_ref() 32 | .and_then(|c| c.nvim()) 33 | .expect("Tabline shouldn't be usable before nvim is initialized") 34 | } 35 | 36 | fn switch_page(&self, idx: u32) { 37 | let target = &self.tabpages[idx as usize]; 38 | if Some(target) != self.selected.as_ref() { 39 | let nvim = self.nvim(); 40 | nvim.block_timeout(nvim.set_current_tabpage(target)) 41 | .report_err(); 42 | } 43 | } 44 | 45 | fn reorder_page(&self, idx: u32, mut new_idx: u32) { 46 | let nvim = self.nvim(); 47 | 48 | // :help :tabm - "N is counted before the move" 49 | if new_idx > idx { 50 | new_idx += 1; 51 | } 52 | 53 | spawn_timeout_user_err!(nvim.command(&format!("{}tabd tabm {new_idx}", idx + 1))); 54 | } 55 | 56 | fn close_tab(&self, idx: u32) { 57 | let nvim = self.nvim(); 58 | spawn_timeout_user_err!(nvim.command(&format!("tabc {}", idx + 1))); 59 | } 60 | } 61 | 62 | pub struct Tabline { 63 | tabs: gtk::Notebook, 64 | state: Rc>, 65 | signal_handlers: [glib::SignalHandlerId; 2], 66 | } 67 | 68 | impl Tabline { 69 | pub fn new() -> Self { 70 | let tabs = gtk::Notebook::builder() 71 | .can_focus(false) 72 | .scrollable(true) 73 | .show_border(false) 74 | .hexpand(true) 75 | .sensitive(false) 76 | .visible(false) 77 | .build(); 78 | 79 | let state = Rc::new(RefCell::new(State::new())); 80 | 81 | Tabline { 82 | tabs: tabs.clone(), 83 | state: state.clone(), 84 | signal_handlers: [ 85 | tabs.connect_switch_page(glib::clone!( 86 | #[strong] 87 | state, 88 | move |_, _, idx| state.borrow().switch_page(idx) 89 | )), 90 | tabs.connect_page_reordered(glib::clone!( 91 | #[strong] 92 | state, 93 | move |_, tab, idx| { 94 | let state = state.borrow(); 95 | state.reorder_page(state.widget_map[tab], idx); 96 | } 97 | )), 98 | ], 99 | } 100 | } 101 | 102 | fn update_state( 103 | &self, 104 | nvim: &Rc, 105 | selected: &Tabpage, 106 | tabs: &[(Tabpage, Option)], 107 | ) { 108 | let mut state = self.state.borrow_mut(); 109 | 110 | if state.nvim.is_none() { 111 | state.nvim = Some(nvim.clone()); 112 | } 113 | 114 | state.selected = Some(selected.clone()); 115 | 116 | state.tabpages = tabs.iter().map(|item| item.0.clone()).collect(); 117 | state.widget_map.clear(); 118 | } 119 | 120 | pub fn update_tabs( 121 | &self, 122 | nvim: &Rc, 123 | selected: Tabpage, 124 | tabs: Vec<(Tabpage, Option)>, 125 | ) { 126 | if tabs.len() <= 1 { 127 | self.tabs.hide(); 128 | return; 129 | } else { 130 | self.tabs.show(); 131 | } 132 | 133 | self.update_state(nvim, &selected, &tabs); 134 | for signal in &self.signal_handlers { 135 | self.block_signal(signal); 136 | } 137 | 138 | let count = self.tabs.n_pages() as usize; 139 | if count < tabs.len() { 140 | for _ in count..tabs.len() { 141 | let empty = gtk::Box::new(gtk::Orientation::Vertical, 0); 142 | let title = gtk::Label::builder() 143 | .ellipsize(pango::EllipsizeMode::Middle) 144 | .width_chars(25) 145 | .hexpand(true) 146 | .build(); 147 | 148 | let close_btn = gtk::Button::from_icon_name("window-close-symbolic"); 149 | close_btn.set_has_frame(false); 150 | close_btn.set_focus_on_click(false); 151 | 152 | let label_box = gtk::Box::builder() 153 | .orientation(gtk::Orientation::Horizontal) 154 | .hexpand(true) 155 | .build(); 156 | label_box.append(&title); 157 | label_box.append(&close_btn); 158 | 159 | self.tabs.append_page(&empty, Some(&label_box)); 160 | self.tabs.set_tab_reorderable(&empty, true); 161 | 162 | let tabs = self.tabs.clone(); 163 | let state_ref = Rc::clone(&self.state); 164 | close_btn.connect_clicked(move |btn| { 165 | let current_label = btn.parent().unwrap(); 166 | for i in 0..tabs.n_pages() { 167 | let page = tabs.nth_page(Some(i)).unwrap(); 168 | let label = tabs.tab_label(&page).unwrap(); 169 | if label == current_label { 170 | state_ref.borrow().close_tab(i); 171 | } 172 | } 173 | }); 174 | } 175 | } else if count > tabs.len() { 176 | for _ in tabs.len()..count { 177 | self.tabs.remove_page(None); 178 | } 179 | } 180 | 181 | let mut state = self.state.borrow_mut(); 182 | 183 | for (idx, tab) in tabs.iter().enumerate() { 184 | let tab_child = self.tabs.nth_page(Some(idx as u32)).unwrap(); 185 | state.widget_map.insert(tab_child.clone(), idx as u32); 186 | 187 | let tab_label = self 188 | .tabs 189 | .tab_label(&tab_child) 190 | .unwrap() 191 | .first_child() 192 | .unwrap() 193 | .downcast::() 194 | .unwrap(); 195 | tab_label.set_text(tab.1.as_ref().unwrap_or(&"??".to_owned())); 196 | 197 | if selected == tab.0 { 198 | self.tabs.set_current_page(Some(idx as u32)); 199 | } 200 | } 201 | 202 | drop(state); 203 | for signal in &self.signal_handlers { 204 | self.unblock_signal(signal); 205 | } 206 | } 207 | } 208 | 209 | impl Deref for Tabline { 210 | type Target = gtk::Notebook; 211 | 212 | fn deref(&self) -> >k::Notebook { 213 | &self.tabs 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /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 std::cell::*; 2 | 3 | use crate::color; 4 | 5 | use gsk::graphene; 6 | 7 | #[derive(Clone)] 8 | pub struct Item { 9 | pub item: pango::Item, 10 | pub cells_count: usize, 11 | glyphs: RefCell>, 12 | /** 13 | * The cached render node for this cell. Note we also need to cache when an item fails to 14 | * generate a valid render node, as pointed out by this testcase: 15 | * https://github.com/Lyude/neovim-gtk/issues/8#issuecomment-1353840913 16 | * Hence the double Option<…> 17 | */ 18 | render_node: RefCell>>, 19 | font: pango::Font, 20 | } 21 | 22 | impl Item { 23 | pub fn new(item: pango::Item, cells_count: usize) -> Self { 24 | debug_assert!(cells_count > 0); 25 | 26 | Item { 27 | font: item.analysis().font(), 28 | item, 29 | cells_count, 30 | glyphs: RefCell::new(None), 31 | render_node: RefCell::new(None), 32 | } 33 | } 34 | 35 | pub fn glyphs(&self) -> Ref> { 36 | self.glyphs.borrow() 37 | } 38 | 39 | pub fn set_glyphs(&mut self, glyphs: pango::GlyphString) { 40 | *self.glyphs.borrow_mut() = Some(glyphs); 41 | *self.render_node.borrow_mut() = None; 42 | } 43 | 44 | pub fn render_node(&self, color: &color::Color, (x, y): (f32, f32)) -> Option { 45 | let mut render_node = self.render_node.borrow_mut(); 46 | if let Some(ref render_node) = *render_node { 47 | render_node.clone() 48 | } else { 49 | let new_render_node = gsk::TextNode::new( 50 | &self.font, 51 | self.glyphs.borrow_mut().as_mut().unwrap(), 52 | &color.into(), 53 | &graphene::Point::new(x, y), 54 | ); 55 | *render_node = Some(new_render_node.clone()); 56 | new_render_node 57 | } 58 | } 59 | 60 | pub fn new_render_node( 61 | &self, 62 | color: &color::Color, 63 | (x, y): (f32, f32), 64 | ) -> Option { 65 | gsk::TextNode::new( 66 | &self.font, 67 | &self.glyphs().as_ref().unwrap().clone(), 68 | &color.into(), 69 | &graphene::Point::new(x, y), 70 | ) 71 | } 72 | 73 | pub fn font(&self) -> &pango::Font { 74 | &self.font 75 | } 76 | 77 | pub fn analysis(&self) -> &pango::Analysis { 78 | self.item.analysis() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /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::{HighlightedLine, HighlightedRange, ModelLayout}; 14 | pub use self::model_rect::ModelRect; 15 | 16 | #[derive(Default)] 17 | pub struct UiModel { 18 | pub columns: usize, 19 | pub rows: usize, 20 | /// (row, col) 21 | cur_pos: (usize, usize), 22 | /// (row, col) 23 | flushed_pos: (usize, usize), 24 | model: Box<[Line]>, 25 | } 26 | 27 | impl UiModel { 28 | pub fn new(rows: u64, columns: u64) -> UiModel { 29 | let mut model = Vec::with_capacity(rows as usize); 30 | for _ in 0..rows as usize { 31 | model.push(Line::new(columns as usize)); 32 | } 33 | 34 | UiModel { 35 | columns: columns as usize, 36 | rows: rows as usize, 37 | cur_pos: (0, 0), 38 | flushed_pos: (0, 0), 39 | model: model.into_boxed_slice(), 40 | } 41 | } 42 | 43 | #[inline] 44 | pub fn model(&self) -> &[Line] { 45 | &self.model 46 | } 47 | 48 | #[inline] 49 | pub fn model_mut(&mut self) -> &mut [Line] { 50 | &mut self.model 51 | } 52 | 53 | /// Get the current point where the cursor is located. Note that this isn't what you want to use 54 | /// if you 55 | pub fn cur_real_point(&self) -> ModelRect { 56 | let (row, col) = self.cur_pos; 57 | ModelRect::point(col, row) 58 | } 59 | 60 | #[inline] 61 | pub fn set_cursor(&mut self, row: usize, col: usize) { 62 | self.cur_pos = (row, col); 63 | } 64 | 65 | #[inline] 66 | pub fn flush_cursor(&mut self) { 67 | self.flushed_pos = self.cur_pos; 68 | } 69 | 70 | /// Get the "real" cursor position, e.g. use the intermediate position if there is one. This is 71 | /// usually what you want for UI model operations 72 | #[inline] 73 | pub fn get_real_cursor(&self) -> (usize, usize) { 74 | self.cur_pos 75 | } 76 | 77 | /// Get the position of the cursor from the last 'flush' event. This is usually what you want 78 | /// for snapshot generation 79 | #[inline] 80 | pub fn get_flushed_cursor(&self) -> (usize, usize) { 81 | self.flushed_pos 82 | } 83 | 84 | pub fn put_one( 85 | &mut self, 86 | row: usize, 87 | col: usize, 88 | ch: &str, 89 | double_width: bool, 90 | hl: Rc, 91 | ) { 92 | self.put(row, col, ch, double_width, 1, hl); 93 | } 94 | 95 | pub fn put( 96 | &mut self, 97 | row: usize, 98 | col: usize, 99 | ch: &str, 100 | double_width: bool, 101 | repeat: usize, 102 | hl: Rc, 103 | ) { 104 | let line = &mut self.model[row]; 105 | line.dirty_line = true; 106 | 107 | for offset in 0..repeat { 108 | let cell = &mut line[col + offset]; 109 | cell.ch.clear(); 110 | cell.ch.push_str(ch); 111 | cell.hl = hl.clone(); 112 | cell.double_width = double_width; 113 | cell.dirty = true; 114 | } 115 | } 116 | 117 | /// Copy rows from 0 to to_row, col from 0 self.columns 118 | /// 119 | /// Don't do any validation! 120 | pub fn swap_rows(&mut self, target: &mut UiModel, to_row: usize) { 121 | for (row_idx, line) in self.model[0..to_row + 1].iter_mut().enumerate() { 122 | let target_row = &mut target.model[row_idx]; 123 | line.swap_with(target_row, 0, self.columns - 1); 124 | } 125 | } 126 | 127 | fn swap_row(&mut self, target_row: i64, offset: i64, left_col: usize, right_col: usize) { 128 | debug_assert_ne!(0, offset); 129 | 130 | let from_row = (target_row + offset) as usize; 131 | 132 | let (left, right) = if offset > 0 { 133 | self.model.split_at_mut(from_row) 134 | } else { 135 | self.model.split_at_mut(target_row as usize) 136 | }; 137 | 138 | let (source_row, target_row) = if offset > 0 { 139 | (&mut right[0], &mut left[target_row as usize]) 140 | } else { 141 | (&mut left[from_row], &mut right[0]) 142 | }; 143 | 144 | source_row.swap_with(target_row, left_col, right_col); 145 | } 146 | 147 | pub fn scroll( 148 | &mut self, 149 | top: i64, 150 | bot: i64, 151 | left: usize, 152 | right: usize, 153 | count: i64, 154 | default_hl: &Rc, 155 | ) { 156 | if count > 0 { 157 | for row in top..(bot - count + 1) { 158 | self.swap_row(row, count, left, right); 159 | } 160 | } else { 161 | for row in ((top - count)..(bot + 1)).rev() { 162 | self.swap_row(row, count, left, right); 163 | } 164 | } 165 | 166 | if count > 0 { 167 | self.clear_region( 168 | (bot - count + 1) as usize, 169 | bot as usize, 170 | left, 171 | right, 172 | default_hl, 173 | ); 174 | } else { 175 | self.clear_region( 176 | top as usize, 177 | (top - count - 1) as usize, 178 | left, 179 | right, 180 | default_hl, 181 | ); 182 | } 183 | } 184 | 185 | pub fn clear(&mut self, default_hl: &Rc) { 186 | let (rows, columns) = (self.rows, self.columns); 187 | self.clear_region(0, rows - 1, 0, columns - 1, default_hl); 188 | } 189 | 190 | fn clear_region( 191 | &mut self, 192 | top: usize, 193 | bot: usize, 194 | left: usize, 195 | right: usize, 196 | default_hl: &Rc, 197 | ) { 198 | for row in &mut self.model[top..bot + 1] { 199 | row.clear(left, right, default_hl); 200 | } 201 | } 202 | 203 | pub fn clear_glyphs(&mut self) { 204 | for row in &mut self.model.iter_mut() { 205 | row.clear_glyphs(); 206 | } 207 | } 208 | } 209 | 210 | #[cfg(test)] 211 | mod tests { 212 | use super::*; 213 | 214 | #[test] 215 | fn test_scroll_area() { 216 | let mut model = UiModel::new(10, 20); 217 | 218 | model.scroll(1, 5, 1, 5, 3, &Rc::new(Highlight::new())); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /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 | #[derive(Clone)] 10 | pub struct HighlightedRange { 11 | pub highlight: Rc, 12 | pub graphemes: Vec, 13 | } 14 | 15 | impl HighlightedRange { 16 | pub fn new(highlight: Rc, graphemes: Vec) -> Self { 17 | Self { 18 | highlight, 19 | graphemes, 20 | } 21 | } 22 | } 23 | 24 | pub type HighlightedLine = Vec; 25 | 26 | pub struct ModelLayout { 27 | pub model: UiModel, 28 | rows_filled: usize, 29 | cols_filled: usize, 30 | lines: Vec, 31 | } 32 | 33 | impl ModelLayout { 34 | const ROWS_STEP: usize = 10; 35 | 36 | pub fn new(columns: u64) -> Self { 37 | ModelLayout { 38 | model: UiModel::new(ModelLayout::ROWS_STEP as u64, columns), 39 | rows_filled: 0, 40 | cols_filled: 0, 41 | lines: Vec::new(), 42 | } 43 | } 44 | 45 | pub fn layout_append(&mut self, mut lines: Vec) { 46 | let rows_filled = self.rows_filled; 47 | let take_from = self.lines.len(); 48 | 49 | self.lines.append(&mut lines); 50 | 51 | self.layout_replace(rows_filled, take_from); 52 | } 53 | 54 | pub fn layout(&mut self, lines: Vec) { 55 | self.lines = lines; 56 | self.layout_replace(0, 0); 57 | } 58 | 59 | pub fn set_cursor(&mut self, col: usize) { 60 | let row = if self.rows_filled > 0 { 61 | self.rows_filled - 1 62 | } else { 63 | 0 64 | }; 65 | 66 | self.model.set_cursor(row, col); 67 | } 68 | 69 | pub fn size(&self) -> (usize, usize) { 70 | ( 71 | max(self.cols_filled, self.model.get_real_cursor().1 + 1), 72 | self.rows_filled, 73 | ) 74 | } 75 | 76 | fn check_model_size(&mut self, rows: usize) { 77 | if rows > self.model.rows { 78 | let model_cols = self.model.columns; 79 | let model_rows = ((rows / (ModelLayout::ROWS_STEP + 1)) + 1) * ModelLayout::ROWS_STEP; 80 | let (cur_row, cur_col) = self.model.get_real_cursor(); 81 | 82 | let mut model = UiModel::new(model_rows as u64, model_cols as u64); 83 | self.model.swap_rows(&mut model, self.rows_filled - 1); 84 | model.set_cursor(cur_row, cur_col); 85 | self.model = model; 86 | } 87 | } 88 | 89 | pub fn insert_char(&mut self, ch: String, shift: bool, hl: Rc) { 90 | if ch.is_empty() { 91 | return; 92 | } 93 | 94 | let (row, col) = self.model.get_real_cursor(); 95 | 96 | if shift { 97 | self.insert_into_lines(ch); 98 | self.layout_replace(0, 0); 99 | } else { 100 | self.model.put_one(row, col, &ch, false, hl); 101 | } 102 | } 103 | 104 | fn insert_into_lines(&mut self, ch: String) { 105 | let highlight_ranges = &mut self.lines[0]; 106 | 107 | let (_, cur_col) = self.model.get_real_cursor(); 108 | 109 | let mut col_idx = 0; 110 | for range in highlight_ranges.iter_mut() { 111 | if cur_col < col_idx + range.graphemes.len() { 112 | let col_sub_idx = cur_col - col_idx; 113 | range.graphemes.insert(col_sub_idx, ch); 114 | break; 115 | } else { 116 | col_idx += range.graphemes.len(); 117 | } 118 | } 119 | } 120 | 121 | /// Wrap all lines into model 122 | /// 123 | /// returns actual width 124 | fn layout_replace(&mut self, row_offset: usize, take_from: usize) { 125 | let rows = ModelLayout::count_lines(&self.lines[take_from..], self.model.columns); 126 | 127 | self.check_model_size(rows + row_offset); 128 | self.rows_filled = rows + row_offset; 129 | 130 | let lines = &self.lines[take_from..]; 131 | 132 | let mut max_col_idx = 0; 133 | let mut col_idx = 0; 134 | let mut row_idx = row_offset; 135 | for highlight_ranges in lines { 136 | for HighlightedRange { 137 | highlight: hl, 138 | graphemes: ch_list, 139 | } in highlight_ranges 140 | { 141 | for ch in ch_list { 142 | let ch_width = max(1, ch.width()); 143 | 144 | if col_idx + ch_width > self.model.columns { 145 | col_idx = 0; 146 | row_idx += 1; 147 | } 148 | 149 | self.model.put_one(row_idx, col_idx, ch, false, hl.clone()); 150 | if ch_width > 1 { 151 | self.model.put_one(row_idx, col_idx, "", true, hl.clone()); 152 | } 153 | 154 | if max_col_idx < col_idx { 155 | max_col_idx = col_idx + ch_width - 1; 156 | } 157 | 158 | col_idx += ch_width; 159 | } 160 | 161 | if col_idx < self.model.columns { 162 | self.model.model[row_idx].clear( 163 | col_idx, 164 | self.model.columns - 1, 165 | &Rc::new(Highlight::new()), 166 | ); 167 | } 168 | } 169 | col_idx = 0; 170 | row_idx += 1; 171 | } 172 | 173 | if self.rows_filled == 1 { 174 | self.cols_filled = max_col_idx + 1; 175 | } else { 176 | self.cols_filled = max(self.cols_filled, max_col_idx + 1); 177 | } 178 | } 179 | 180 | fn count_lines(lines: &[HighlightedLine], max_columns: usize) -> usize { 181 | let mut row_count = 0; 182 | 183 | for line in lines { 184 | let len: usize = line.iter().map(|range| range.graphemes.len()).sum(); 185 | row_count += len / (max_columns + 1) + 1; 186 | } 187 | 188 | row_count 189 | } 190 | } 191 | 192 | #[cfg(test)] 193 | mod tests { 194 | use super::*; 195 | 196 | #[test] 197 | fn test_count_lines() { 198 | let lines = vec![vec![HighlightedRange { 199 | highlight: Rc::new(Highlight::new()), 200 | graphemes: vec!["a".to_owned(); 5], 201 | }]]; 202 | 203 | let rows = ModelLayout::count_lines(&lines, 4); 204 | assert_eq!(2, rows); 205 | } 206 | 207 | #[test] 208 | fn test_resize() { 209 | let lines = vec![ 210 | vec![HighlightedRange { 211 | highlight: Rc::new(Highlight::new()), 212 | graphemes: vec!["a".to_owned(); 5] 213 | }]; 214 | ModelLayout::ROWS_STEP 215 | ]; 216 | let mut model = ModelLayout::new(5); 217 | 218 | model.layout(lines.clone()); 219 | let (cols, rows) = model.size(); 220 | assert_eq!(5, cols); 221 | assert_eq!(ModelLayout::ROWS_STEP, rows); 222 | 223 | model.layout_append(lines); 224 | let (cols, rows) = model.size(); 225 | assert_eq!(5, cols); 226 | assert_eq!(ModelLayout::ROWS_STEP * 2, rows); 227 | assert_eq!(ModelLayout::ROWS_STEP * 2, model.model.rows); 228 | } 229 | 230 | #[test] 231 | fn test_cols_filled() { 232 | let lines = vec![ 233 | vec![HighlightedRange::new( 234 | Rc::new(Highlight::new()), 235 | vec!["a".to_owned(); 3] 236 | )]; 237 | 1 238 | ]; 239 | let mut model = ModelLayout::new(5); 240 | 241 | model.layout(lines); 242 | // cursor is not moved by newgrid api 243 | // so set it manual 244 | model.set_cursor(3); 245 | let (cols, _) = model.size(); 246 | assert_eq!(4, cols); // size is 3 and 4 - is with cursor position 247 | 248 | let lines = vec![ 249 | vec![HighlightedRange::new( 250 | Rc::new(Highlight::new()), 251 | vec!["a".to_owned(); 2] 252 | )]; 253 | 1 254 | ]; 255 | 256 | model.layout_append(lines); 257 | model.set_cursor(2); 258 | let (cols, _) = model.size(); 259 | assert_eq!(3, cols); 260 | } 261 | 262 | #[test] 263 | fn test_insert_shift() { 264 | let lines = vec![ 265 | vec![HighlightedRange::new( 266 | Rc::new(Highlight::new()), 267 | vec!["a".to_owned(); 3] 268 | )]; 269 | 1 270 | ]; 271 | let mut model = ModelLayout::new(5); 272 | model.layout(lines); 273 | model.set_cursor(1); 274 | 275 | model.insert_char("b".to_owned(), true, Rc::new(Highlight::new())); 276 | 277 | let (cols, _) = model.size(); 278 | assert_eq!(4, cols); 279 | assert_eq!("b", model.model.model()[0].line[1].ch); 280 | } 281 | 282 | #[test] 283 | fn test_insert_no_shift() { 284 | let lines = vec![ 285 | vec![HighlightedRange::new( 286 | Rc::new(Highlight::new()), 287 | vec!["a".to_owned(); 3] 288 | )]; 289 | 1 290 | ]; 291 | let mut model = ModelLayout::new(5); 292 | model.layout(lines); 293 | model.set_cursor(1); 294 | 295 | model.insert_char("b".to_owned(), false, Rc::new(Highlight::new())); 296 | 297 | let (cols, _) = model.size(); 298 | assert_eq!(3, cols); 299 | assert_eq!("b", model.model.model()[0].line[1].ch); 300 | } 301 | 302 | #[test] 303 | fn test_double_width() { 304 | let lines = vec![ 305 | vec![HighlightedRange::new( 306 | Rc::new(Highlight::new()), 307 | vec!["あ".to_owned(); 3] 308 | )]; 309 | 1 310 | ]; 311 | let mut model = ModelLayout::new(7); 312 | model.layout(lines); 313 | model.set_cursor(1); 314 | 315 | let (cols, rows) = model.size(); 316 | assert_eq!(1, rows); 317 | assert_eq!(6, cols); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/ui_model/model_rect.rs: -------------------------------------------------------------------------------- 1 | use super::UiModel; 2 | use crate::render::CellMetrics; 3 | 4 | #[derive(Clone, PartialEq, Eq, Debug)] 5 | pub struct ModelRect { 6 | pub top: usize, 7 | pub bot: usize, 8 | pub left: usize, 9 | pub right: usize, 10 | } 11 | 12 | impl ModelRect { 13 | pub fn new(top: usize, bot: usize, left: usize, right: usize) -> ModelRect { 14 | // FIXME: Add back formatted messages here once new clippy updates propagate downstream 15 | debug_assert!(top <= bot); 16 | debug_assert!(left <= right); 17 | 18 | ModelRect { 19 | top, 20 | bot, 21 | left, 22 | right, 23 | } 24 | } 25 | 26 | pub fn point(x: usize, y: usize) -> ModelRect { 27 | ModelRect { 28 | top: y, 29 | bot: y, 30 | left: x, 31 | right: x, 32 | } 33 | } 34 | 35 | /// Extend rect to left and right to make changed Item rerendered 36 | pub fn extend_by_items(&mut self, model: Option<&UiModel>) { 37 | if model.is_none() { 38 | return; 39 | } 40 | let model = model.unwrap(); 41 | 42 | let mut left = self.left; 43 | let mut right = self.right; 44 | 45 | for i in self.top..self.bot + 1 { 46 | let line = &model.model[i]; 47 | let item_idx = line.cell_to_item(self.left); 48 | if item_idx >= 0 { 49 | let item_idx = item_idx as usize; 50 | if item_idx < left { 51 | left = item_idx; 52 | } 53 | } 54 | 55 | let len_since_right = line.item_len_from_idx(self.right) - 1; 56 | if right < self.right + len_since_right { 57 | right = self.right + len_since_right; 58 | } 59 | 60 | // extend also double_width chars 61 | let cell = &line.line[self.left]; 62 | if self.left > 0 && cell.double_width { 63 | let dw_char_idx = self.left - 1; 64 | if dw_char_idx < left { 65 | left = dw_char_idx; 66 | } 67 | } 68 | 69 | let dw_char_idx = self.right + 1; 70 | if let Some(cell) = line.line.get(dw_char_idx) { 71 | if cell.double_width && right < dw_char_idx { 72 | right = dw_char_idx; 73 | } 74 | } 75 | } 76 | 77 | self.left = left; 78 | self.right = right; 79 | } 80 | 81 | pub fn join(&mut self, rect: &ModelRect) { 82 | self.top = if self.top < rect.top { 83 | self.top 84 | } else { 85 | rect.top 86 | }; 87 | self.left = if self.left < rect.left { 88 | self.left 89 | } else { 90 | rect.left 91 | }; 92 | 93 | self.bot = if self.bot > rect.bot { 94 | self.bot 95 | } else { 96 | rect.bot 97 | }; 98 | self.right = if self.right > rect.right { 99 | self.right 100 | } else { 101 | rect.right 102 | }; 103 | 104 | debug_assert!(self.top <= self.bot); 105 | debug_assert!(self.left <= self.right); 106 | } 107 | 108 | pub fn to_area(&self, cell_metrics: &CellMetrics) -> (i32, i32, i32, i32) { 109 | let &CellMetrics { 110 | char_width, 111 | line_height, 112 | .. 113 | } = cell_metrics; 114 | 115 | // when convert to i32 area must be bigger then original f64 version 116 | ( 117 | (self.left as f64 * char_width).floor() as i32, 118 | (self.top as f64 * line_height).floor() as i32, 119 | ((self.right - self.left + 1) as f64 * char_width).ceil() as i32, 120 | ((self.bot - self.top + 1) as f64 * line_height).ceil() as i32, 121 | ) 122 | } 123 | } 124 | 125 | impl AsRef for ModelRect { 126 | fn as_ref(&self) -> &ModelRect { 127 | self 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/value.rs: -------------------------------------------------------------------------------- 1 | use nvim_rs::Value; 2 | use std::collections::HashMap; 3 | 4 | pub trait ValueMapExt { 5 | fn to_attrs_map(&self) -> Result, String>; 6 | } 7 | 8 | impl ValueMapExt for Vec<(Value, Value)> { 9 | fn to_attrs_map(&self) -> Result, String> { 10 | self.iter() 11 | .map(|p| { 12 | p.0.as_str() 13 | .ok_or_else(|| "Can't convert map key to string".to_owned()) 14 | .map(|key| (key, &p.1)) 15 | }) 16 | .collect::, String>>() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/cli_tests.rs: -------------------------------------------------------------------------------- 1 | /// Tests for the command line interface (e.g. `nvim-gtk --no-fork foo.txt`) 2 | 3 | #[test] 4 | fn cli_tests() { 5 | trycmd::TestCases::new() 6 | .env("NVIM_GTK_CLI_TEST_MODE", "1") 7 | .case("tests/cmd/*.trycmd"); 8 | #[cfg(unix)] 9 | trycmd::TestCases::new() 10 | .env("NVIM_GTK_CLI_TEST_MODE", "1") 11 | .case("tests/cmd_unix/*.trycmd"); 12 | } 13 | -------------------------------------------------------------------------------- /tests/cmd/arg_parsing.trycmd: -------------------------------------------------------------------------------- 1 | Make sure `$NVIM_GTK_CLI_TEST_MODE` actually works 2 | 3 | ``` 4 | $ nvim-gtk 5 | ? success 6 | Testing the CLI 7 | 8 | ``` 9 | 10 | Diff mode tests 11 | 12 | ``` 13 | $ nvim-gtk -d 14 | ? failed 15 | error: the following required arguments were not provided: 16 | ... 17 | 18 | Usage: nvim-gtk[EXE] -d ... [-- ...] 19 | 20 | For more information, try '--help'. 21 | 22 | $ nvim-gtk -d foo 23 | ? failed 24 | error: Diff mode (-d) requires 2 or more files 25 | 26 | Usage: nvim-gtk[EXE] [OPTIONS] [FILES]... [-- ...] 27 | 28 | For more information, try '--help'. 29 | 30 | $ nvim-gtk -d foo bar 31 | ? success 32 | Testing the CLI 33 | 34 | ``` 35 | 36 | Post-config commands 37 | 38 | ``` 39 | $ nvim-gtk -c 40 | ? failed 41 | error: a value is required for '-c ' but none was supplied 42 | 43 | For more information, try '--help'. 44 | 45 | $ nvim-gtk -c foo 46 | ? success 47 | Testing the CLI 48 | Commands passed: ['foo'] 49 | 50 | $ nvim-gtk -c foo -c 51 | ? failed 52 | error: a value is required for '-c ' but none was supplied 53 | 54 | For more information, try '--help'. 55 | 56 | $ nvim-gtk -c foo -c bar 57 | ? success 58 | Testing the CLI 59 | Commands passed: ['foo', 'bar'] 60 | 61 | ``` 62 | -------------------------------------------------------------------------------- /tests/cmd_unix/unix_pipes.trycmd: -------------------------------------------------------------------------------- 1 | Invalid paths for Unix sockets should fail immediately 2 | 3 | ``` 4 | $ nvim-gtk --server /tmp/foo 5 | ? failed 6 | error: invalid value '/tmp/foo' for '--server ': No such file or directory (os error 2) 7 | 8 | For more information, try '--help'. 9 | 10 | ``` 11 | --------------------------------------------------------------------------------