├── .github └── workflows │ ├── linux.yml │ └── mac.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── rust-toolchain └── src ├── application.rs ├── document.rs ├── info_box.rs ├── main.rs ├── notification.rs ├── overlay.rs ├── picker.rs ├── prompt.rs ├── statusline.rs ├── utils.rs └── workspace.rs /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Build for linux 2 | on: [pull_request] 3 | 4 | jobs: 5 | check: 6 | name: cargo check 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: dtolnay/rust-toolchain@stable 11 | - run: cargo check 12 | 13 | test: 14 | name: cargo test 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: dtolnay/rust-toolchain@stable 19 | - uses: awalsh128/cache-apt-pkgs-action@latest 20 | with: 21 | packages: libxkbcommon-dev libxkbcommon-x11-dev 22 | version: 1.0 23 | - run: cargo test 24 | 25 | build: 26 | name: cargo build 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - uses: dtolnay/rust-toolchain@stable 31 | - uses: awalsh128/cache-apt-pkgs-action@latest 32 | with: 33 | packages: libxkbcommon-dev libxkbcommon-x11-dev 34 | version: 1.0 35 | - run: cargo build --release 36 | -------------------------------------------------------------------------------- /.github/workflows/mac.yml: -------------------------------------------------------------------------------- 1 | name: Build for mac 2 | on: [pull_request] 3 | 4 | jobs: 5 | check: 6 | name: cargo check 7 | runs-on: macos-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: dtolnay/rust-toolchain@stable 11 | - run: cargo check 12 | 13 | test: 14 | name: cargo test 15 | runs-on: macos-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: dtolnay/rust-toolchain@stable 19 | - run: cargo test 20 | 21 | build: 22 | name: cargo build 23 | runs-on: macos-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: dtolnay/rust-toolchain@stable 27 | - run: cargo build --release 28 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "helix-gpui" 3 | edition = "2021" 4 | authors = ["Alexander Polakov ", "Blaž Hrastnik "] 5 | description = "A post-modern text editor." 6 | include = ["src/**/*", "README.md"] 7 | default-run = "hxg" 8 | version = "0.0.1" 9 | license = "MPL-2.0" 10 | 11 | [features] 12 | default = ["git"] 13 | unicode-lines = ["helix-core/unicode-lines"] 14 | integration = ["helix-event/integration_test"] 15 | git = ["helix-vcs/git"] 16 | 17 | [[bin]] 18 | name = "hxg" 19 | path = "src/main.rs" 20 | 21 | [dependencies] 22 | gpui = { git = "https://github.com/zed-industries/zed" } 23 | 24 | helix-stdx = { git = "https://github.com/polachok/helix", rev = "31651eab32b4a950a2dd86c6b30f1f7b3c854979" } 25 | helix-core = { git = "https://github.com/polachok/helix", rev = "31651eab32b4a950a2dd86c6b30f1f7b3c854979" } 26 | helix-event = { git = "https://github.com/polachok/helix" , rev = "31651eab32b4a950a2dd86c6b30f1f7b3c854979" } 27 | helix-view = { git = "https://github.com/polachok/helix" , rev = "31651eab32b4a950a2dd86c6b30f1f7b3c854979" } 28 | helix-lsp = { git = "https://github.com/polachok/helix" , rev = "31651eab32b4a950a2dd86c6b30f1f7b3c854979" } 29 | # helix-dap = { git = "https://github.com/polachok/helix" } 30 | helix-vcs = { git = "https://github.com/polachok/helix" , rev = "31651eab32b4a950a2dd86c6b30f1f7b3c854979" } 31 | helix-loader = { git = "https://github.com/polachok/helix" , rev = "31651eab32b4a950a2dd86c6b30f1f7b3c854979" } 32 | helix-term = { git = "https://github.com/polachok/helix" , rev = "31651eab32b4a950a2dd86c6b30f1f7b3c854979" } 33 | tui = { git = "https://github.com/polachok/helix", package = "helix-tui", default-features = false, rev = "31651eab32b4a950a2dd86c6b30f1f7b3c854979" } 34 | 35 | anyhow = "1" 36 | once_cell = "1.19" 37 | 38 | tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } 39 | crossterm = { version = "0.27", features = ["event-stream"] } 40 | signal-hook = "0.3" 41 | tokio-stream = "0.1" 42 | futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } 43 | arc-swap = { version = "1.7.1" } 44 | termini = "1" 45 | 46 | # Logging 47 | fern = "0.6" 48 | chrono = { version = "0.4", default-features = false, features = ["clock"] } 49 | log = "0.4" 50 | 51 | # File picker 52 | nucleo = "0.2" 53 | ignore = "0.4" 54 | # markdown doc rendering 55 | pulldown-cmark = { version = "0.10", default-features = false } 56 | # file type detection 57 | content_inspector = "0.2.4" 58 | 59 | # opening URLs 60 | open = "5.1.2" 61 | url = "2.5.0" 62 | 63 | # config 64 | toml = "0.8" 65 | 66 | serde_json = "1.0" 67 | serde = { version = "1.0", features = ["derive"] } 68 | 69 | # ripgrep for global search 70 | grep-regex = "0.1.12" 71 | grep-searcher = "0.1.13" 72 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Helix gpui 2 | Screenshot 2024-05-26 at 20 57 39 3 | 4 | This is a simple GUI for [helix](https://helix-editor.com/) editor. Most modal editors are terminal apps, I'd like to change that and implement a good modal GUI editor. 5 | 6 | ## State of things 7 | Currently this project has *less* features and more bugs than helix-term (`hx`). 8 | 9 | * The first goal is to reach feature parity 10 | * Second goal is refactoring helix to allow for implementing commands properly 11 | * Third goal is adding things like builtin terminal and file tree 12 | -------------------------------------------------------------------------------- /rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.77.1" 3 | components = ["rustfmt", "rust-src", "clippy"] 4 | -------------------------------------------------------------------------------- /src/application.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::btree_map::Entry, path::Path, sync::Arc}; 2 | 3 | use arc_swap::{access::Map, ArcSwap}; 4 | use futures_util::FutureExt; 5 | use helix_core::diagnostic::Severity; 6 | use helix_core::{pos_at_coords, syntax, Position, Selection}; 7 | 8 | use helix_lsp::{ 9 | lsp::{self, notification::Notification}, 10 | LanguageServerId, LspProgressMap, 11 | }; 12 | use helix_stdx::path::get_relative_path; 13 | use helix_term::job::Jobs; 14 | use helix_term::{ 15 | args::Args, compositor::Compositor, config::Config, keymap::Keymaps, ui::EditorView, 16 | }; 17 | use helix_view::document::DocumentSavedEventResult; 18 | use helix_view::{doc_mut, graphics::Rect, handlers::Handlers, theme, Editor}; 19 | 20 | use anyhow::Error; 21 | use log::{debug, error, info, warn}; 22 | use serde_json::json; 23 | use tokio_stream::StreamExt; 24 | 25 | pub struct Application { 26 | pub editor: Editor, 27 | pub compositor: Compositor, 28 | pub view: EditorView, 29 | pub jobs: Jobs, 30 | pub lsp_progress: LspProgressMap, 31 | } 32 | 33 | #[derive(Debug, Clone)] 34 | pub enum InputEvent { 35 | Key(helix_view::input::KeyEvent), 36 | ScrollLines { 37 | line_count: usize, 38 | direction: helix_core::movement::Direction, 39 | view_id: helix_view::ViewId, 40 | }, 41 | } 42 | 43 | pub struct Input; 44 | 45 | impl gpui::EventEmitter for Input {} 46 | 47 | pub struct Crank; 48 | 49 | impl gpui::EventEmitter<()> for Crank {} 50 | 51 | impl Application { 52 | fn emit_overlays(&mut self, cx: &mut gpui::ModelContext<'_, crate::Core>) { 53 | use crate::picker::Picker as PickerComponent; 54 | use crate::prompt::Prompt; 55 | use helix_term::ui::{overlay::Overlay, Picker}; 56 | use std::path::PathBuf; 57 | 58 | let picker = if let Some(p) = self 59 | .compositor 60 | .find_id::>>(helix_term::ui::picker::ID) 61 | { 62 | println!("found file picker"); 63 | Some(PickerComponent::make(&mut self.editor, &mut p.content)) 64 | } else { 65 | None 66 | }; 67 | let prompt = if let Some(p) = self.compositor.find::() { 68 | Some(Prompt::make(&mut self.editor, p)) 69 | } else { 70 | None 71 | }; 72 | 73 | if let Some(picker) = picker { 74 | cx.emit(crate::Update::Picker(picker)); 75 | } 76 | 77 | if let Some(prompt) = prompt { 78 | cx.emit(crate::Update::Prompt(prompt)); 79 | } 80 | 81 | if let Some(info) = self.editor.autoinfo.take() { 82 | cx.emit(crate::Update::Info(info)); 83 | } 84 | } 85 | 86 | pub fn handle_input_event( 87 | &mut self, 88 | event: InputEvent, 89 | cx: &mut gpui::ModelContext<'_, crate::Core>, 90 | handle: tokio::runtime::Handle, 91 | ) { 92 | let _guard = handle.enter(); 93 | use helix_term::compositor::{Component, EventResult}; 94 | // println!("INPUT EVENT {:?}", event); 95 | 96 | let mut comp_ctx = helix_term::compositor::Context { 97 | editor: &mut self.editor, 98 | scroll: None, 99 | jobs: &mut self.jobs, 100 | }; 101 | match event { 102 | InputEvent::Key(key) => { 103 | let mut is_handled = self 104 | .compositor 105 | .handle_event(&helix_view::input::Event::Key(key), &mut comp_ctx); 106 | if !is_handled { 107 | let event = &helix_view::input::Event::Key(key); 108 | let res = self.view.handle_event(event, &mut comp_ctx); 109 | is_handled = matches!(res, EventResult::Consumed(_)); 110 | if let EventResult::Consumed(Some(cb)) = res { 111 | cb(&mut self.compositor, &mut comp_ctx); 112 | } 113 | } 114 | let _is_handled = is_handled; 115 | // println!("KEY IS HANDLED ? {:?}", is_handled); 116 | self.emit_overlays(cx); 117 | cx.emit(crate::Update::Redraw); 118 | } 119 | InputEvent::ScrollLines { 120 | line_count, 121 | direction, 122 | .. 123 | } => { 124 | let mut ctx = helix_term::commands::Context { 125 | editor: &mut self.editor, 126 | register: None, 127 | count: None, 128 | callback: Vec::new(), 129 | on_next_key_callback: None, 130 | jobs: &mut self.jobs, 131 | }; 132 | helix_term::commands::scroll(&mut ctx, line_count, direction, false); 133 | cx.emit(crate::Update::Redraw); 134 | } 135 | } 136 | } 137 | 138 | fn handle_document_write(&mut self, doc_save_event: &DocumentSavedEventResult) { 139 | let doc_save_event = match doc_save_event { 140 | Ok(event) => event, 141 | Err(err) => { 142 | self.editor.set_error(err.to_string()); 143 | return; 144 | } 145 | }; 146 | 147 | let doc = match self.editor.document_mut(doc_save_event.doc_id) { 148 | None => { 149 | warn!( 150 | "received document saved event for non-existent doc id: {}", 151 | doc_save_event.doc_id 152 | ); 153 | 154 | return; 155 | } 156 | Some(doc) => doc, 157 | }; 158 | 159 | debug!( 160 | "document {:?} saved with revision {}", 161 | doc.path(), 162 | doc_save_event.revision 163 | ); 164 | 165 | doc.set_last_saved_revision(doc_save_event.revision); 166 | 167 | let lines = doc_save_event.text.len_lines(); 168 | let bytes = doc_save_event.text.len_bytes(); 169 | 170 | self.editor 171 | .set_doc_path(doc_save_event.doc_id, &doc_save_event.path); 172 | // TODO: fix being overwritten by lsp 173 | self.editor.set_status(format!( 174 | "'{}' written, {}L {}B", 175 | get_relative_path(&doc_save_event.path).to_string_lossy(), 176 | lines, 177 | bytes 178 | )); 179 | } 180 | 181 | pub fn handle_crank_event( 182 | &mut self, 183 | _event: (), 184 | cx: &mut gpui::ModelContext<'_, crate::Core>, 185 | handle: tokio::runtime::Handle, 186 | ) { 187 | let _guard = handle.enter(); 188 | 189 | self.step(cx).now_or_never(); 190 | /* 191 | use std::future::Future; 192 | let fut = self.step(cx); 193 | let mut fut = Box::pin(fut); 194 | handle.block_on(std::future::poll_fn(move |cx| { 195 | let _ = fut.as_mut().poll(cx); 196 | Poll::Ready(()) 197 | })); 198 | */ 199 | } 200 | 201 | pub async fn step(&mut self, cx: &mut gpui::ModelContext<'_, crate::Core>) { 202 | loop { 203 | tokio::select! { 204 | biased; 205 | 206 | // Some(event) = input_stream.next() => { 207 | // // self.handle_input_event(event, cx); 208 | // //self.handle_terminal_events(event).await; 209 | // } 210 | Some(callback) = self.jobs.callbacks.recv() => { 211 | self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback))); 212 | // self.render().await; 213 | } 214 | Some(msg) = self.jobs.status_messages.recv() => { 215 | let severity = match msg.severity{ 216 | helix_event::status::Severity::Hint => Severity::Hint, 217 | helix_event::status::Severity::Info => Severity::Info, 218 | helix_event::status::Severity::Warning => Severity::Warning, 219 | helix_event::status::Severity::Error => Severity::Error, 220 | }; 221 | let status = crate::EditorStatus { status: msg.message.to_string(), severity }; 222 | cx.emit(crate::Update::EditorStatus(status)); 223 | // TODO: show multiple status messages at once to avoid clobbering 224 | self.editor.status_msg = Some((msg.message, severity)); 225 | helix_event::request_redraw(); 226 | } 227 | Some(callback) = self.jobs.wait_futures.next() => { 228 | self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); 229 | // self.render().await; 230 | } 231 | event = self.editor.wait_event() => { 232 | use helix_view::editor::EditorEvent; 233 | match event { 234 | EditorEvent::DocumentSaved(event) => { 235 | self.handle_document_write(&event); 236 | cx.emit(crate::Update::EditorEvent(EditorEvent::DocumentSaved(event))); 237 | } 238 | EditorEvent::IdleTimer => { 239 | self.editor.clear_idle_timer(); 240 | /* dont send */ 241 | } 242 | EditorEvent::Redraw => { 243 | cx.emit(crate::Update::EditorEvent(EditorEvent::Redraw)); 244 | } 245 | EditorEvent::ConfigEvent(_) => { 246 | /* TODO */ 247 | } 248 | EditorEvent::LanguageServerMessage((id, call)) => { 249 | self.handle_language_server_message(call, id).await; 250 | } 251 | EditorEvent::DebuggerEvent(_) => { 252 | /* TODO */ 253 | } 254 | } 255 | } 256 | else => { 257 | break; 258 | } 259 | } 260 | } 261 | } 262 | 263 | // copy pasted from helix_term/src/application.rs 264 | async fn handle_language_server_message( 265 | &mut self, 266 | call: helix_lsp::Call, 267 | server_id: LanguageServerId, 268 | ) { 269 | use helix_lsp::{Call, MethodCall, Notification}; 270 | 271 | macro_rules! language_server { 272 | () => { 273 | match self.editor.language_server_by_id(server_id) { 274 | Some(language_server) => language_server, 275 | None => { 276 | warn!("can't find language server with id `{}`", server_id); 277 | return; 278 | } 279 | } 280 | }; 281 | } 282 | 283 | match call { 284 | Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => { 285 | let notification = match Notification::parse(&method, params) { 286 | Ok(notification) => notification, 287 | Err(helix_lsp::Error::Unhandled) => { 288 | info!("Ignoring Unhandled notification from Language Server"); 289 | return; 290 | } 291 | Err(err) => { 292 | error!( 293 | "Ignoring unknown notification from Language Server: {}", 294 | err 295 | ); 296 | return; 297 | } 298 | }; 299 | 300 | match notification { 301 | Notification::Initialized => { 302 | let language_server = language_server!(); 303 | 304 | // Trigger a workspace/didChangeConfiguration notification after initialization. 305 | // This might not be required by the spec but Neovim does this as well, so it's 306 | // probably a good idea for compatibility. 307 | if let Some(config) = language_server.config() { 308 | tokio::spawn(language_server.did_change_configuration(config.clone())); 309 | } 310 | 311 | let docs = self 312 | .editor 313 | .documents() 314 | .filter(|doc| doc.supports_language_server(server_id)); 315 | 316 | // trigger textDocument/didOpen for docs that are already open 317 | for doc in docs { 318 | let url = match doc.url() { 319 | Some(url) => url, 320 | None => continue, // skip documents with no path 321 | }; 322 | 323 | let language_id = 324 | doc.language_id().map(ToOwned::to_owned).unwrap_or_default(); 325 | 326 | tokio::spawn(language_server.text_document_did_open( 327 | url, 328 | doc.version(), 329 | doc.text(), 330 | language_id, 331 | )); 332 | } 333 | } 334 | Notification::PublishDiagnostics(mut params) => { 335 | let path = match params.uri.to_file_path() { 336 | Ok(path) => helix_stdx::path::normalize(path), 337 | Err(_) => { 338 | log::error!("Unsupported file URI: {}", params.uri); 339 | return; 340 | } 341 | }; 342 | let language_server = language_server!(); 343 | if !language_server.is_initialized() { 344 | log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name()); 345 | return; 346 | } 347 | // have to inline the function because of borrow checking... 348 | let doc = self.editor.documents.values_mut() 349 | .find(|doc| doc.path().map(|p| p == &path).unwrap_or(false)) 350 | .filter(|doc| { 351 | if let Some(version) = params.version { 352 | if version != doc.version() { 353 | log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version()); 354 | return false; 355 | } 356 | } 357 | true 358 | }); 359 | 360 | let mut unchanged_diag_sources = Vec::new(); 361 | if let Some(doc) = &doc { 362 | let lang_conf = doc.language.clone(); 363 | 364 | if let Some(lang_conf) = &lang_conf { 365 | if let Some(old_diagnostics) = self.editor.diagnostics.get(&path) { 366 | if !lang_conf.persistent_diagnostic_sources.is_empty() { 367 | // Sort diagnostics first by severity and then by line numbers. 368 | // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order 369 | params 370 | .diagnostics 371 | .sort_unstable_by_key(|d| (d.severity, d.range.start)); 372 | } 373 | for source in &lang_conf.persistent_diagnostic_sources { 374 | let new_diagnostics = params 375 | .diagnostics 376 | .iter() 377 | .filter(|d| d.source.as_ref() == Some(source)); 378 | let old_diagnostics = old_diagnostics 379 | .iter() 380 | .filter(|(d, d_server)| { 381 | *d_server == server_id 382 | && d.source.as_ref() == Some(source) 383 | }) 384 | .map(|(d, _)| d); 385 | if new_diagnostics.eq(old_diagnostics) { 386 | unchanged_diag_sources.push(source.clone()) 387 | } 388 | } 389 | } 390 | } 391 | } 392 | 393 | let diagnostics = params.diagnostics.into_iter().map(|d| (d, server_id)); 394 | 395 | // Insert the original lsp::Diagnostics here because we may have no open document 396 | // for diagnosic message and so we can't calculate the exact position. 397 | // When using them later in the diagnostics picker, we calculate them on-demand. 398 | let diagnostics = match self.editor.diagnostics.entry(path) { 399 | Entry::Occupied(o) => { 400 | let current_diagnostics = o.into_mut(); 401 | // there may entries of other language servers, which is why we can't overwrite the whole entry 402 | current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id); 403 | current_diagnostics.extend(diagnostics); 404 | current_diagnostics 405 | // Sort diagnostics first by severity and then by line numbers. 406 | } 407 | Entry::Vacant(v) => v.insert(diagnostics.collect()), 408 | }; 409 | 410 | // Sort diagnostics first by severity and then by line numbers. 411 | // Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order 412 | diagnostics.sort_unstable_by_key(|(d, server_id)| { 413 | (d.severity, d.range.start, *server_id) 414 | }); 415 | 416 | if let Some(doc) = doc { 417 | let diagnostic_of_language_server_and_not_in_unchanged_sources = 418 | |diagnostic: &lsp::Diagnostic, ls_id| { 419 | ls_id == server_id 420 | && diagnostic.source.as_ref().map_or(true, |source| { 421 | !unchanged_diag_sources.contains(source) 422 | }) 423 | }; 424 | let diagnostics = Editor::doc_diagnostics_with_filter( 425 | &self.editor.language_servers, 426 | &self.editor.diagnostics, 427 | doc, 428 | diagnostic_of_language_server_and_not_in_unchanged_sources, 429 | ); 430 | doc.replace_diagnostics( 431 | diagnostics, 432 | &unchanged_diag_sources, 433 | Some(server_id), 434 | ); 435 | } 436 | } 437 | Notification::ShowMessage(params) => { 438 | log::warn!("unhandled window/showMessage: {:?}", params); 439 | } 440 | Notification::LogMessage(params) => { 441 | log::info!("window/logMessage: {:?}", params); 442 | } 443 | Notification::ProgressMessage(_params) => { 444 | // if !self 445 | // .compositor 446 | // .has_component(std::any::type_name::()) => 447 | // { 448 | // let editor_view = self 449 | // .compositor 450 | // .find::() 451 | // .expect("expected at least one EditorView"); 452 | // let lsp::ProgressParams { token, value } = params; 453 | 454 | // let lsp::ProgressParamsValue::WorkDone(work) = value; 455 | // let parts = match &work { 456 | // lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin { 457 | // title, 458 | // message, 459 | // percentage, 460 | // .. 461 | // }) => (Some(title), message, percentage), 462 | // lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport { 463 | // message, 464 | // percentage, 465 | // .. 466 | // }) => (None, message, percentage), 467 | // lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message }) => { 468 | // if message.is_some() { 469 | // (None, message, &None) 470 | // } else { 471 | // self.lsp_progress.end_progress(server_id, &token); 472 | // if !self.lsp_progress.is_progressing(server_id) { 473 | // editor_view.spinners_mut().get_or_create(server_id).stop(); 474 | // } 475 | // self.editor.clear_status(); 476 | 477 | // // we want to render to clear any leftover spinners or messages 478 | // return; 479 | // } 480 | // } 481 | // }; 482 | 483 | // let token_d: &dyn std::fmt::Display = match &token { 484 | // lsp::NumberOrString::Number(n) => n, 485 | // lsp::NumberOrString::String(s) => s, 486 | // }; 487 | 488 | // let status = match parts { 489 | // (Some(title), Some(message), Some(percentage)) => { 490 | // format!("[{}] {}% {} - {}", token_d, percentage, title, message) 491 | // } 492 | // (Some(title), None, Some(percentage)) => { 493 | // format!("[{}] {}% {}", token_d, percentage, title) 494 | // } 495 | // (Some(title), Some(message), None) => { 496 | // format!("[{}] {} - {}", token_d, title, message) 497 | // } 498 | // (None, Some(message), Some(percentage)) => { 499 | // format!("[{}] {}% {}", token_d, percentage, message) 500 | // } 501 | // (Some(title), None, None) => { 502 | // format!("[{}] {}", token_d, title) 503 | // } 504 | // (None, Some(message), None) => { 505 | // format!("[{}] {}", token_d, message) 506 | // } 507 | // (None, None, Some(percentage)) => { 508 | // format!("[{}] {}%", token_d, percentage) 509 | // } 510 | // (None, None, None) => format!("[{}]", token_d), 511 | // }; 512 | 513 | // if let lsp::WorkDoneProgress::End(_) = work { 514 | // self.lsp_progress.end_progress(server_id, &token); 515 | // if !self.lsp_progress.is_progressing(server_id) { 516 | // editor_view.spinners_mut().get_or_create(server_id).stop(); 517 | // } 518 | // } else { 519 | // self.lsp_progress.update(server_id, token, work); 520 | // } 521 | 522 | // if self.config.load().editor.lsp.display_messages { 523 | // self.editor.set_status(status); 524 | // } 525 | } 526 | Notification::ProgressMessage(_params) => { 527 | // do nothing 528 | } 529 | Notification::Exit => { 530 | self.editor.set_status("Language server exited"); 531 | 532 | // LSPs may produce diagnostics for files that haven't been opened in helix, 533 | // we need to clear those and remove the entries from the list if this leads to 534 | // an empty diagnostic list for said files 535 | for diags in self.editor.diagnostics.values_mut() { 536 | diags.retain(|(_, lsp_id)| *lsp_id != server_id); 537 | } 538 | 539 | self.editor.diagnostics.retain(|_, diags| !diags.is_empty()); 540 | 541 | // Clear any diagnostics for documents with this server open. 542 | for doc in self.editor.documents_mut() { 543 | doc.clear_diagnostics(Some(server_id)); 544 | } 545 | 546 | // Remove the language server from the registry. 547 | self.editor.language_servers.remove_by_id(server_id); 548 | } 549 | } 550 | } 551 | Call::MethodCall(helix_lsp::jsonrpc::MethodCall { 552 | method, params, id, .. 553 | }) => { 554 | let reply = match MethodCall::parse(&method, params) { 555 | Err(helix_lsp::Error::Unhandled) => { 556 | error!( 557 | "Language Server: Method {} not found in request {}", 558 | method, id 559 | ); 560 | Err(helix_lsp::jsonrpc::Error { 561 | code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, 562 | message: format!("Method not found: {}", method), 563 | data: None, 564 | }) 565 | } 566 | Err(err) => { 567 | log::error!( 568 | "Language Server: Received malformed method call {} in request {}: {}", 569 | method, 570 | id, 571 | err 572 | ); 573 | Err(helix_lsp::jsonrpc::Error { 574 | code: helix_lsp::jsonrpc::ErrorCode::ParseError, 575 | message: format!("Malformed method call: {}", method), 576 | data: None, 577 | }) 578 | } 579 | Ok(MethodCall::WorkDoneProgressCreate(params)) => { 580 | self.lsp_progress.create(server_id, params.token); 581 | 582 | // let editor_view = self 583 | // .compositor 584 | // .find::() 585 | // .expect("expected at least one EditorView"); 586 | // let spinner = editor_view.spinners_mut().get_or_create(server_id); 587 | // if spinner.is_stopped() { 588 | // spinner.start(); 589 | // } 590 | 591 | Ok(serde_json::Value::Null) 592 | } 593 | Ok(MethodCall::ApplyWorkspaceEdit(params)) => { 594 | let language_server = language_server!(); 595 | if language_server.is_initialized() { 596 | let offset_encoding = language_server.offset_encoding(); 597 | let res = self 598 | .editor 599 | .apply_workspace_edit(offset_encoding, ¶ms.edit); 600 | 601 | Ok(json!(lsp::ApplyWorkspaceEditResponse { 602 | applied: res.is_ok(), 603 | failure_reason: res.as_ref().err().map(|err| err.kind.to_string()), 604 | failed_change: res 605 | .as_ref() 606 | .err() 607 | .map(|err| err.failed_change_idx as u32), 608 | })) 609 | } else { 610 | Err(helix_lsp::jsonrpc::Error { 611 | code: helix_lsp::jsonrpc::ErrorCode::InvalidRequest, 612 | message: "Server must be initialized to request workspace edits" 613 | .to_string(), 614 | data: None, 615 | }) 616 | } 617 | } 618 | Ok(MethodCall::WorkspaceFolders) => { 619 | Ok(json!(&*language_server!().workspace_folders().await)) 620 | } 621 | Ok(MethodCall::WorkspaceConfiguration(params)) => { 622 | let language_server = language_server!(); 623 | let result: Vec<_> = params 624 | .items 625 | .iter() 626 | .map(|item| { 627 | let mut config = language_server.config()?; 628 | if let Some(section) = item.section.as_ref() { 629 | // for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server') 630 | if !section.is_empty() { 631 | for part in section.split('.') { 632 | config = config.get(part)?; 633 | } 634 | } 635 | } 636 | Some(config) 637 | }) 638 | .collect(); 639 | Ok(json!(result)) 640 | } 641 | Ok(MethodCall::RegisterCapability(params)) => { 642 | if let Some(client) = self.editor.language_servers.get_by_id(server_id) { 643 | for reg in params.registrations { 644 | match reg.method.as_str() { 645 | lsp::notification::DidChangeWatchedFiles::METHOD => { 646 | let Some(options) = reg.register_options else { 647 | continue; 648 | }; 649 | let ops: lsp::DidChangeWatchedFilesRegistrationOptions = 650 | match serde_json::from_value(options) { 651 | Ok(ops) => ops, 652 | Err(err) => { 653 | log::warn!("Failed to deserialize DidChangeWatchedFilesRegistrationOptions: {err}"); 654 | continue; 655 | } 656 | }; 657 | self.editor.language_servers.file_event_handler.register( 658 | client.id(), 659 | Arc::downgrade(client), 660 | reg.id, 661 | ops, 662 | ) 663 | } 664 | _ => { 665 | // Language Servers based on the `vscode-languageserver-node` library often send 666 | // client/registerCapability even though we do not enable dynamic registration 667 | // for most capabilities. We should send a MethodNotFound JSONRPC error in this 668 | // case but that rejects the registration promise in the server which causes an 669 | // exit. So we work around this by ignoring the request and sending back an OK 670 | // response. 671 | log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server"); 672 | } 673 | } 674 | } 675 | } 676 | 677 | Ok(serde_json::Value::Null) 678 | } 679 | Ok(MethodCall::UnregisterCapability(params)) => { 680 | for unreg in params.unregisterations { 681 | match unreg.method.as_str() { 682 | lsp::notification::DidChangeWatchedFiles::METHOD => { 683 | self.editor 684 | .language_servers 685 | .file_event_handler 686 | .unregister(server_id, unreg.id); 687 | } 688 | _ => { 689 | log::warn!("Received unregistration request for unsupported method: {}", unreg.method); 690 | } 691 | } 692 | } 693 | Ok(serde_json::Value::Null) 694 | } 695 | Ok(MethodCall::ShowDocument(_params)) => { 696 | // let language_server = language_server!(); 697 | // let offset_encoding = language_server.offset_encoding(); 698 | 699 | // let result = self.handle_show_document(params, offset_encoding); 700 | let result = lsp::ShowDocumentResult { success: true }; 701 | Ok(json!(result)) 702 | } 703 | }; 704 | 705 | tokio::spawn(language_server!().reply(id, reply)); 706 | } 707 | Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id), 708 | } 709 | } 710 | } 711 | 712 | pub fn init_editor( 713 | args: Args, 714 | config: Config, 715 | lang_loader: syntax::Loader, 716 | ) -> Result { 717 | use helix_view::editor::Action; 718 | 719 | let mut theme_parent_dirs = vec![helix_loader::config_dir()]; 720 | theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); 721 | let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs)); 722 | 723 | let true_color = true; 724 | let theme = config 725 | .theme 726 | .as_ref() 727 | .and_then(|theme| { 728 | theme_loader 729 | .load(theme) 730 | .map_err(|e| { 731 | log::warn!("failed to load theme `{}` - {}", theme, e); 732 | e 733 | }) 734 | .ok() 735 | .filter(|theme| (true_color || theme.is_16_color())) 736 | }) 737 | .unwrap_or_else(|| theme_loader.default_theme(true_color)); 738 | 739 | let syn_loader = Arc::new(ArcSwap::from_pointee(lang_loader)); 740 | let config = Arc::new(ArcSwap::from_pointee(config)); 741 | 742 | let area = Rect { 743 | x: 0, 744 | y: 0, 745 | width: 80, 746 | height: 25, 747 | }; 748 | let (tx, _rx) = tokio::sync::mpsc::channel(1); 749 | let (tx1, _rx1) = tokio::sync::mpsc::channel(1); 750 | let handlers = Handlers { 751 | completions: tx, 752 | signature_hints: tx1, 753 | }; 754 | let mut editor = Editor::new( 755 | area, 756 | theme_loader.clone(), 757 | syn_loader.clone(), 758 | Arc::new(Map::new(Arc::clone(&config), |config: &Config| { 759 | &config.editor 760 | })), 761 | handlers, 762 | ); 763 | 764 | if args.load_tutor { 765 | let path = helix_loader::runtime_file(Path::new("tutor")); 766 | // let path = Path::new("./test.rs"); 767 | let doc_id = editor.open(&path, Action::VerticalSplit)?; 768 | let view_id = editor.tree.focus; 769 | let doc = doc_mut!(editor, &doc_id); 770 | let pos = Selection::point(pos_at_coords( 771 | doc.text().slice(..), 772 | Position::new(0, 0), 773 | true, 774 | )); 775 | doc.set_selection(view_id, pos); 776 | 777 | // Unset path to prevent accidentally saving to the original tutor file. 778 | doc_mut!(editor).set_path(None); 779 | } else { 780 | editor.new_file(Action::VerticalSplit); 781 | } 782 | 783 | editor.set_theme(theme); 784 | 785 | let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| { 786 | &config.keys 787 | })); 788 | let compositor = Compositor::new(Rect { 789 | x: 0, 790 | y: 0, 791 | width: 80, 792 | height: 25, 793 | }); 794 | let keymaps = Keymaps::new(keys); 795 | let view = EditorView::new(keymaps); 796 | let jobs = Jobs::new(); 797 | 798 | helix_term::events::register(); 799 | 800 | Ok(Application { 801 | editor, 802 | compositor, 803 | view, 804 | jobs, 805 | lsp_progress: LspProgressMap::new(), 806 | }) 807 | } 808 | -------------------------------------------------------------------------------- /src/document.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use gpui::{prelude::FluentBuilder, *}; 4 | use helix_core::{ 5 | ropey::RopeSlice, 6 | syntax::{Highlight, HighlightEvent}, 7 | }; 8 | use helix_lsp::lsp::{Diagnostic, DiagnosticSeverity, NumberOrString}; 9 | use helix_term::ui::EditorView; 10 | use helix_view::{graphics::CursorKind, Document, DocumentId, Editor, Theme, View, ViewId}; 11 | use log::debug; 12 | 13 | use crate::utils::color_to_hsla; 14 | use crate::{Core, Input, InputEvent}; 15 | 16 | pub struct DocumentView { 17 | core: Model, 18 | input: Model, 19 | view_id: ViewId, 20 | style: TextStyle, 21 | focus: FocusHandle, 22 | is_focused: bool, 23 | } 24 | 25 | impl DocumentView { 26 | pub fn new( 27 | core: Model, 28 | input: Model, 29 | view_id: ViewId, 30 | style: TextStyle, 31 | focus: &FocusHandle, 32 | is_focused: bool, 33 | ) -> Self { 34 | Self { 35 | core, 36 | input, 37 | view_id, 38 | style, 39 | focus: focus.clone(), 40 | is_focused, 41 | } 42 | } 43 | 44 | pub fn set_focused(&mut self, is_focused: bool) { 45 | self.is_focused = is_focused; 46 | } 47 | 48 | fn get_diagnostics(&self, cx: &mut ViewContext) -> Vec { 49 | if !self.is_focused { 50 | return Vec::new(); 51 | } 52 | 53 | let core = self.core.read(cx); 54 | let editor = &core.editor; 55 | 56 | let (cursor_pos, doc_id, first_row) = { 57 | let view = editor.tree.get(self.view_id); 58 | let doc_id = view.doc; 59 | let document = editor.document(doc_id).unwrap(); 60 | let text = document.text(); 61 | 62 | let primary_idx = document 63 | .selection(self.view_id) 64 | .primary() 65 | .cursor(text.slice(..)); 66 | let cursor_pos = view.screen_coords_at_pos(document, text.slice(..), primary_idx); 67 | 68 | let anchor = view.offset.anchor; 69 | let first_row = text.char_to_line(anchor.min(text.len_chars())); 70 | (cursor_pos, doc_id, first_row) 71 | }; 72 | let Some(cursor_pos) = cursor_pos else { 73 | return Vec::new(); 74 | }; 75 | 76 | let mut diags = Vec::new(); 77 | if let Some(path) = editor.document(doc_id).and_then(|doc| doc.path()).cloned() { 78 | if let Some(diagnostics) = editor.diagnostics.get(&path) { 79 | for (diag, _) in diagnostics.iter().filter(|(diag, _)| { 80 | let (start_line, end_line) = 81 | (diag.range.start.line as usize, diag.range.end.line as usize); 82 | let row = cursor_pos.row + first_row; 83 | start_line <= row && row <= end_line 84 | }) { 85 | diags.push(diag.clone()); 86 | } 87 | } 88 | } 89 | diags 90 | } 91 | } 92 | 93 | impl EventEmitter for DocumentView {} 94 | 95 | impl Render for DocumentView { 96 | fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { 97 | println!("{:?}: rendering document view", self.view_id); 98 | 99 | cx.on_focus_out(&self.focus, |this, cx| { 100 | let is_focused = this.focus.is_focused(cx); 101 | 102 | if this.is_focused != is_focused { 103 | this.is_focused = is_focused; 104 | cx.notify(); 105 | } 106 | debug!( 107 | "{:?} document view focus changed OUT: {:?}", 108 | this.view_id, this.is_focused 109 | ); 110 | }) 111 | .detach(); 112 | 113 | cx.on_focus_in(&self.focus, |this, cx| { 114 | let is_focused = this.focus.is_focused(cx); 115 | 116 | if this.is_focused != is_focused { 117 | this.is_focused = is_focused; 118 | cx.notify(); 119 | } 120 | debug!( 121 | "{:?} document view focus changed IN: {:?}", 122 | this.view_id, this.is_focused 123 | ); 124 | }) 125 | .detach(); 126 | 127 | let doc_id = { 128 | let editor = &self.core.read(cx).editor; 129 | let view = editor.tree.get(self.view_id); 130 | view.doc 131 | }; 132 | 133 | let handle = ScrollHandle::default(); 134 | let doc = DocumentElement::new( 135 | self.core.clone(), 136 | doc_id.clone(), 137 | self.view_id.clone(), 138 | self.style.clone(), 139 | &self.focus, 140 | self.is_focused, 141 | ) 142 | .overflow_y_scroll() 143 | .track_scroll(&handle) 144 | .on_scroll_wheel(cx.listener(move |view, ev: &ScrollWheelEvent, cx| { 145 | use helix_core::movement::Direction; 146 | let view_id = view.view_id; 147 | let line_height = view.style.line_height_in_pixels(cx.rem_size()); 148 | 149 | debug!("SCROLL WHEEL {:?}", ev); 150 | let delta = ev.delta.pixel_delta(line_height); 151 | if delta.y != px(0.) { 152 | let lines = delta.y / line_height; 153 | let direction = if lines > 0. { 154 | Direction::Backward 155 | } else { 156 | Direction::Forward 157 | }; 158 | let line_count = 1 + lines.abs() as usize; 159 | 160 | view.input.update(cx, |_, cx| { 161 | cx.emit(InputEvent::ScrollLines { 162 | direction, 163 | line_count, 164 | view_id, 165 | }) 166 | }); 167 | } 168 | })); 169 | 170 | let status = crate::statusline::StatusLine::new( 171 | self.core.clone(), 172 | doc_id.clone(), 173 | self.view_id, 174 | self.is_focused, 175 | self.style.clone(), 176 | ); 177 | 178 | let diags = { 179 | let theme = self.core.read(cx).editor.theme.clone(); 180 | 181 | self.get_diagnostics(cx).into_iter().map(move |diag| { 182 | cx.new_view(|_| DiagnosticView { 183 | diagnostic: diag, 184 | theme: theme.clone(), 185 | }) 186 | }) 187 | }; 188 | 189 | div() 190 | .w_full() 191 | .h_full() 192 | .flex() 193 | .flex_col() 194 | .child(doc) 195 | .child(status) 196 | .child( 197 | div() 198 | .flex() 199 | .w(DefiniteLength::Fraction(0.33)) 200 | .h(DefiniteLength::Fraction(0.8)) 201 | .flex_col() 202 | .absolute() 203 | .top_8() 204 | .right_5() 205 | .gap_4() 206 | .children(diags), 207 | ) 208 | } 209 | } 210 | 211 | impl FocusableView for DocumentView { 212 | fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { 213 | self.focus.clone() 214 | } 215 | } 216 | 217 | pub struct DocumentElement { 218 | core: Model, 219 | doc_id: DocumentId, 220 | view_id: ViewId, 221 | style: TextStyle, 222 | interactivity: Interactivity, 223 | focus: FocusHandle, 224 | is_focused: bool, 225 | } 226 | 227 | impl IntoElement for DocumentElement { 228 | type Element = Self; 229 | 230 | fn into_element(self) -> Self { 231 | self 232 | } 233 | } 234 | 235 | impl DocumentElement { 236 | pub fn new( 237 | core: Model, 238 | doc_id: DocumentId, 239 | view_id: ViewId, 240 | style: TextStyle, 241 | focus: &FocusHandle, 242 | is_focused: bool, 243 | ) -> Self { 244 | Self { 245 | core, 246 | doc_id, 247 | view_id, 248 | style, 249 | interactivity: Interactivity::default(), 250 | focus: focus.clone(), 251 | is_focused, 252 | } 253 | .track_focus(&focus) 254 | .element 255 | } 256 | 257 | // These 3 methods are just proxies for EditorView 258 | // TODO: make a PR to helix to extract them from helix_term into helix_view or smth. 259 | fn doc_diagnostics_highlights<'d>( 260 | doc: &'d helix_view::Document, 261 | theme: &Theme, 262 | ) -> impl Iterator)>> { 263 | EditorView::doc_diagnostics_highlights(doc, theme).into_iter() 264 | } 265 | 266 | fn doc_syntax_highlights<'d>( 267 | doc: &'d helix_view::Document, 268 | anchor: usize, 269 | height: u16, 270 | theme: &Theme, 271 | ) -> Box + 'd> { 272 | EditorView::doc_syntax_highlights(doc, anchor, height, theme) 273 | } 274 | 275 | fn doc_selection_highlights( 276 | mode: helix_view::document::Mode, 277 | doc: &Document, 278 | view: &View, 279 | theme: &Theme, 280 | cursor_shape_config: &helix_view::editor::CursorShapeConfig, 281 | is_window_focused: bool, 282 | ) -> Vec<(usize, std::ops::Range)> { 283 | EditorView::doc_selection_highlights( 284 | mode, 285 | doc, 286 | view, 287 | theme, 288 | cursor_shape_config, 289 | is_window_focused, 290 | ) 291 | } 292 | 293 | fn overlay_highlights( 294 | mode: helix_view::document::Mode, 295 | doc: &Document, 296 | view: &View, 297 | theme: &Theme, 298 | cursor_shape_config: &helix_view::editor::CursorShapeConfig, 299 | is_window_focused: bool, 300 | is_view_focused: bool, 301 | ) -> impl Iterator { 302 | let mut overlay_highlights = 303 | EditorView::empty_highlight_iter(doc, view.offset.anchor, view.inner_area(doc).height); 304 | if is_view_focused { 305 | let highlights = helix_core::syntax::merge( 306 | overlay_highlights, 307 | Self::doc_selection_highlights( 308 | mode, 309 | doc, 310 | view, 311 | theme, 312 | cursor_shape_config, 313 | is_window_focused, 314 | ), 315 | ); 316 | let focused_view_elements = 317 | EditorView::highlight_focused_view_elements(view, doc, theme); 318 | if focused_view_elements.is_empty() { 319 | overlay_highlights = Box::new(highlights) 320 | } else { 321 | overlay_highlights = 322 | Box::new(helix_core::syntax::merge(highlights, focused_view_elements)) 323 | } 324 | } 325 | 326 | for diagnostic in Self::doc_diagnostics_highlights(doc, theme) { 327 | // Most of the `diagnostic` Vecs are empty most of the time. Skipping 328 | // a merge for any empty Vec saves a significant amount of work. 329 | if diagnostic.is_empty() { 330 | continue; 331 | } 332 | overlay_highlights = 333 | Box::new(helix_core::syntax::merge(overlay_highlights, diagnostic)); 334 | } 335 | 336 | overlay_highlights 337 | } 338 | 339 | fn highlight( 340 | editor: &Editor, 341 | doc: &Document, 342 | view: &View, 343 | theme: &Theme, 344 | is_view_focused: bool, 345 | anchor: usize, 346 | lines: u16, 347 | end_char: usize, 348 | fg_color: Hsla, 349 | font: Font, 350 | ) -> Vec { 351 | let mut runs = vec![]; 352 | let overlay_highlights = Self::overlay_highlights( 353 | editor.mode(), 354 | doc, 355 | view, 356 | theme, 357 | &editor.config().cursor_shape, 358 | true, 359 | is_view_focused, 360 | ); 361 | 362 | let syntax_highlights = Self::doc_syntax_highlights(doc, anchor, lines, theme); 363 | 364 | let mut syntax_styles = StyleIter { 365 | text_style: helix_view::graphics::Style::default(), 366 | active_highlights: Vec::with_capacity(64), 367 | highlight_iter: syntax_highlights, 368 | theme, 369 | }; 370 | 371 | let mut overlay_styles = StyleIter { 372 | text_style: helix_view::graphics::Style::default(), 373 | active_highlights: Vec::with_capacity(64), 374 | highlight_iter: overlay_highlights, 375 | theme, 376 | }; 377 | 378 | let mut syntax_span = 379 | syntax_styles 380 | .next() 381 | .unwrap_or((helix_view::graphics::Style::default(), 0, usize::MAX)); 382 | let mut overlay_span = overlay_styles.next().unwrap_or(( 383 | helix_view::graphics::Style::default(), 384 | 0, 385 | usize::MAX, 386 | )); 387 | 388 | let mut position = anchor; 389 | loop { 390 | let (syn_style, syn_start, syn_end) = syntax_span; 391 | let (ovl_style, ovl_start, ovl_end) = overlay_span; 392 | 393 | /* if we are between highlights, insert default style */ 394 | let (style, is_default) = if position < syn_start && position < ovl_start { 395 | (helix_view::graphics::Style::default(), true) 396 | } else { 397 | let mut style = helix_view::graphics::Style::default(); 398 | if position >= syn_start && position < syn_end { 399 | style = style.patch(syn_style); 400 | } 401 | if position >= ovl_start && position < ovl_end { 402 | style = style.patch(ovl_style); 403 | } 404 | (style, false) 405 | }; 406 | 407 | let fg = style 408 | .fg 409 | .and_then(|fg| color_to_hsla(fg)) 410 | .unwrap_or(fg_color); 411 | let bg = style.bg.and_then(|bg| color_to_hsla(bg)); 412 | let len = if is_default { 413 | std::cmp::min(syn_start, ovl_start) - position 414 | } else { 415 | std::cmp::min( 416 | syn_end.checked_sub(position).unwrap_or(usize::MAX), 417 | ovl_end.checked_sub(position).unwrap_or(usize::MAX), 418 | ) 419 | }; 420 | let underline = style.underline_color.and_then(color_to_hsla); 421 | let underline = underline.map(|color| UnderlineStyle { 422 | thickness: px(1.), 423 | color: Some(color), 424 | wavy: true, 425 | }); 426 | 427 | let len = std::cmp::min(len, end_char); 428 | 429 | let run = TextRun { 430 | len, 431 | font: font.clone(), 432 | color: fg, 433 | background_color: bg, 434 | underline, 435 | strikethrough: None, 436 | }; 437 | runs.push(run); 438 | position += len; 439 | 440 | if position >= end_char { 441 | break; 442 | } 443 | if position >= syn_end { 444 | syntax_span = syntax_styles.next().unwrap_or((style, 0, usize::MAX)); 445 | } 446 | if position >= ovl_end { 447 | overlay_span = overlay_styles.next().unwrap_or((style, 0, usize::MAX)); 448 | } 449 | } 450 | runs 451 | } 452 | } 453 | 454 | impl InteractiveElement for DocumentElement { 455 | fn interactivity(&mut self) -> &mut Interactivity { 456 | &mut self.interactivity 457 | } 458 | } 459 | 460 | impl StatefulInteractiveElement for DocumentElement {} 461 | 462 | #[derive(Debug)] 463 | #[allow(unused)] 464 | pub struct DocumentLayout { 465 | rows: usize, 466 | columns: usize, 467 | line_height: Pixels, 468 | font_size: Pixels, 469 | cell_width: Pixels, 470 | hitbox: Option, 471 | } 472 | 473 | struct RopeWrapper<'a>(RopeSlice<'a>); 474 | 475 | impl<'a> Into for RopeWrapper<'a> { 476 | fn into(self) -> SharedString { 477 | let cow: Cow<'_, str> = self.0.into(); 478 | cow.to_string().into() // this is crazy 479 | } 480 | } 481 | 482 | impl Element for DocumentElement { 483 | type RequestLayoutState = (); 484 | 485 | type PrepaintState = DocumentLayout; 486 | 487 | fn id(&self) -> Option { 488 | None 489 | } 490 | 491 | fn request_layout( 492 | &mut self, 493 | _id: Option<&GlobalElementId>, 494 | cx: &mut WindowContext, 495 | ) -> (LayoutId, Self::RequestLayoutState) { 496 | let mut style = Style::default(); 497 | style.size.width = relative(1.).into(); 498 | style.size.height = relative(1.).into(); 499 | let layout_id = cx.request_layout(style, []); 500 | (layout_id, ()) 501 | } 502 | 503 | fn prepaint( 504 | &mut self, 505 | id: Option<&GlobalElementId>, 506 | bounds: Bounds, 507 | _before_layout: &mut Self::RequestLayoutState, 508 | cx: &mut WindowContext, 509 | ) -> Self::PrepaintState { 510 | debug!("editor bounds {:?}", bounds); 511 | let core = self.core.clone(); 512 | self.interactivity 513 | .prepaint(id, bounds, bounds.size, cx, |_, _, hitbox, cx| { 514 | cx.with_content_mask(Some(ContentMask { bounds }), |cx| { 515 | let font_id = cx.text_system().resolve_font(&self.style.font()); 516 | let font_size = self.style.font_size.to_pixels(cx.rem_size()); 517 | let line_height = self.style.line_height_in_pixels(cx.rem_size()); 518 | let em_width = cx 519 | .text_system() 520 | .typographic_bounds(font_id, font_size, 'm') 521 | .unwrap() 522 | .size 523 | .width; 524 | let cell_width = cx 525 | .text_system() 526 | .advance(font_id, font_size, 'm') 527 | .unwrap() 528 | .width; 529 | let columns = (bounds.size.width / em_width).floor() as usize; 530 | let rows = (bounds.size.height / line_height).floor() as usize; 531 | 532 | core.update(cx, |core, _cx| { 533 | let rect = helix_view::graphics::Rect { 534 | x: 0, 535 | y: 0, 536 | width: columns as u16, 537 | height: rows as u16, 538 | }; 539 | let editor = &mut core.editor; 540 | editor.resize(rect) 541 | }); 542 | DocumentLayout { 543 | hitbox, 544 | rows, 545 | columns, 546 | line_height, 547 | font_size, 548 | cell_width, 549 | } 550 | }) 551 | }) 552 | } 553 | 554 | fn paint( 555 | &mut self, 556 | id: Option<&GlobalElementId>, 557 | bounds: Bounds, 558 | _: &mut Self::RequestLayoutState, 559 | after_layout: &mut Self::PrepaintState, 560 | cx: &mut WindowContext, 561 | ) { 562 | let focus = self.focus.clone(); 563 | self.interactivity 564 | .on_mouse_down(MouseButton::Left, move |_ev, cx| { 565 | println!("MOUSE DOWN"); 566 | cx.focus(&focus); 567 | }); 568 | 569 | let is_focused = self.is_focused; 570 | 571 | self.interactivity 572 | .paint(id, bounds, after_layout.hitbox.as_ref(), cx, |_, cx| { 573 | let core = self.core.read(cx); 574 | let editor = &core.editor; 575 | 576 | let view = editor.tree.get(self.view_id); 577 | let _viewport = view.area; 578 | 579 | let theme = &editor.theme; 580 | let default_style = theme.get("ui.background"); 581 | let bg_color = color_to_hsla(default_style.bg.unwrap()).unwrap_or(black()); 582 | let cursor_style = theme.get("ui.cursor.primary"); 583 | let bg = fill(bounds, bg_color); 584 | let fg_color = color_to_hsla( 585 | default_style 586 | .fg 587 | .unwrap_or(helix_view::graphics::Color::White), 588 | ) 589 | .unwrap_or(white()); 590 | 591 | let document = editor.document(self.doc_id).unwrap(); 592 | let text = document.text(); 593 | 594 | let (_, cursor_kind) = editor.cursor(); 595 | let primary_idx = document 596 | .selection(self.view_id) 597 | .primary() 598 | .cursor(text.slice(..)); 599 | let cursor_pos = view.screen_coords_at_pos(document, text.slice(..), primary_idx); 600 | 601 | let gutter_width = view.gutter_offset(document); 602 | let gutter_overflow = gutter_width == 0; 603 | if !gutter_overflow { 604 | debug!("need to render gutter {}", gutter_width); 605 | } 606 | 607 | let cursor_text = None; // TODO 608 | 609 | let _cursor_row = cursor_pos.map(|p| p.row); 610 | let anchor = view.offset.anchor; 611 | let total_lines = text.len_lines(); 612 | let first_row = text.char_to_line(anchor.min(text.len_chars())); 613 | // println!("first row is {}", row); 614 | let last_row = (first_row + after_layout.rows + 1).min(total_lines); 615 | // println!("first row is {first_row} last row is {last_row}"); 616 | let end_char = text.line_to_char(std::cmp::min(last_row, total_lines)); 617 | 618 | let text_view = text.slice(anchor..end_char); 619 | let str: SharedString = RopeWrapper(text_view).into(); 620 | 621 | let runs = Self::highlight( 622 | &editor, 623 | document, 624 | view, 625 | theme, 626 | self.is_focused, 627 | anchor, 628 | total_lines.min(after_layout.rows + 1) as u16, 629 | end_char, 630 | fg_color, 631 | self.style.font(), 632 | ); 633 | let shaped_lines = cx 634 | .text_system() 635 | .shape_text(str, after_layout.font_size, &runs, None) 636 | .unwrap(); 637 | 638 | cx.paint_quad(bg); 639 | 640 | let mut origin = bounds.origin; 641 | origin.x += px(2.) + (after_layout.cell_width * gutter_width as f32); 642 | origin.y += px(1.); 643 | 644 | // draw document 645 | for line in shaped_lines { 646 | line.paint(origin, after_layout.line_height, cx).unwrap(); 647 | origin.y += after_layout.line_height; 648 | } 649 | // draw cursor 650 | if self.is_focused { 651 | match (cursor_pos, cursor_kind) { 652 | (Some(position), kind) => { 653 | let helix_core::Position { row, col } = position; 654 | let origin_y = after_layout.line_height * row as f32; 655 | let origin_x = 656 | after_layout.cell_width * ((col + gutter_width as usize) as f32); 657 | let mut cursor_fg = cursor_style 658 | .bg 659 | .and_then(|fg| color_to_hsla(fg)) 660 | .unwrap_or(fg_color); 661 | cursor_fg.a = 0.5; 662 | 663 | let mut cursor = Cursor { 664 | origin: gpui::Point::new(origin_x, origin_y), 665 | kind, 666 | color: cursor_fg, 667 | block_width: after_layout.cell_width, 668 | line_height: after_layout.line_height, 669 | text: cursor_text, 670 | }; 671 | let mut origin = bounds.origin; 672 | origin.x += px(2.); 673 | origin.y += px(1.); 674 | 675 | cursor.paint(origin, cx); 676 | } 677 | (None, _) => {} 678 | } 679 | } 680 | // draw gutter 681 | { 682 | let mut gutter_origin = bounds.origin; 683 | gutter_origin.x += px(2.); 684 | gutter_origin.y += px(1.); 685 | 686 | let core = self.core.read(cx); 687 | let editor = &core.editor; 688 | let theme = &editor.theme; 689 | let view = editor.tree.get(self.view_id); 690 | let document = editor.document(self.doc_id).unwrap(); 691 | let lines = (first_row..last_row) 692 | .enumerate() 693 | .map(|(visual_line, doc_line)| LinePos { 694 | first_visual_line: true, 695 | doc_line, 696 | visual_line: visual_line as u16, 697 | start_char_idx: 0, 698 | }); 699 | 700 | let mut gutter = Gutter { 701 | after_layout, 702 | text_system: cx.text_system().clone(), 703 | lines: Vec::new(), 704 | style: self.style.clone(), 705 | origin: gutter_origin, 706 | }; 707 | { 708 | let mut gutters = Vec::new(); 709 | Gutter::init_gutter( 710 | &editor, 711 | document, 712 | view, 713 | theme, 714 | is_focused, 715 | &mut gutters, 716 | ); 717 | for line in lines { 718 | for gut in &mut gutters { 719 | gut(line, &mut gutter) 720 | } 721 | } 722 | } 723 | for (origin, line) in gutter.lines { 724 | line.paint(origin, after_layout.line_height, cx).unwrap(); 725 | } 726 | } 727 | }); 728 | } 729 | } 730 | 731 | struct Gutter<'a> { 732 | after_layout: &'a DocumentLayout, 733 | text_system: std::sync::Arc, 734 | lines: Vec<(Point, ShapedLine)>, 735 | style: TextStyle, 736 | origin: Point, 737 | } 738 | 739 | impl<'a> Gutter<'a> { 740 | fn init_gutter<'d>( 741 | editor: &'d Editor, 742 | doc: &'d Document, 743 | view: &'d View, 744 | theme: &Theme, 745 | is_focused: bool, 746 | gutters: &mut Vec>, 747 | ) { 748 | let text = doc.text().slice(..); 749 | let cursors: std::rc::Rc<[_]> = doc 750 | .selection(view.id) 751 | .iter() 752 | .map(|range| range.cursor_line(text)) 753 | .collect(); 754 | 755 | let mut offset = 0; 756 | 757 | let gutter_style = theme.get("ui.gutter"); 758 | let gutter_selected_style = theme.get("ui.gutter.selected"); 759 | let gutter_style_virtual = theme.get("ui.gutter.virtual"); 760 | let gutter_selected_style_virtual = theme.get("ui.gutter.selected.virtual"); 761 | 762 | for gutter_type in view.gutters() { 763 | let mut gutter = gutter_type.style(editor, doc, view, theme, is_focused); 764 | let width = gutter_type.width(view, doc); 765 | // avoid lots of small allocations by reusing a text buffer for each line 766 | let mut text = String::with_capacity(width); 767 | let cursors = cursors.clone(); 768 | let gutter_decoration = move |pos: LinePos, renderer: &mut Self| { 769 | // TODO handle softwrap in gutters 770 | let selected = cursors.contains(&pos.doc_line); 771 | let x = offset; 772 | let y = pos.visual_line; 773 | 774 | let gutter_style = match (selected, pos.first_visual_line) { 775 | (false, true) => gutter_style, 776 | (true, true) => gutter_selected_style, 777 | (false, false) => gutter_style_virtual, 778 | (true, false) => gutter_selected_style_virtual, 779 | }; 780 | 781 | if let Some(style) = 782 | gutter(pos.doc_line, selected, pos.first_visual_line, &mut text) 783 | { 784 | renderer.render(x, y, width, gutter_style.patch(style), Some(&text)); 785 | } else { 786 | renderer.render(x, y, width, gutter_style, None); 787 | } 788 | text.clear(); 789 | }; 790 | gutters.push(Box::new(gutter_decoration)); 791 | 792 | offset += width as u16; 793 | } 794 | } 795 | } 796 | 797 | impl<'a> GutterRenderer for Gutter<'a> { 798 | fn render( 799 | &mut self, 800 | x: u16, 801 | y: u16, 802 | _width: usize, 803 | style: helix_view::graphics::Style, 804 | text: Option<&str>, 805 | ) { 806 | let origin_y = self.origin.y + self.after_layout.line_height * y as f32; 807 | let origin_x = self.origin.x + self.after_layout.cell_width * x as f32; 808 | 809 | let fg_color = style 810 | .fg 811 | .and_then(color_to_hsla) 812 | .unwrap_or(hsla(0., 0., 1., 1.)); 813 | if let Some(text) = text { 814 | let run = TextRun { 815 | len: text.len(), 816 | font: self.style.font(), 817 | color: fg_color, 818 | background_color: None, 819 | underline: None, 820 | strikethrough: None, 821 | }; 822 | let shaped = self 823 | .text_system 824 | .shape_line(text.to_string().into(), self.after_layout.font_size, &[run]) 825 | .unwrap(); 826 | self.lines.push(( 827 | Point { 828 | x: origin_x, 829 | y: origin_y, 830 | }, 831 | shaped, 832 | )); 833 | } 834 | } 835 | } 836 | 837 | struct Cursor { 838 | origin: gpui::Point, 839 | kind: CursorKind, 840 | color: Hsla, 841 | block_width: Pixels, 842 | line_height: Pixels, 843 | text: Option, 844 | } 845 | 846 | impl Cursor { 847 | fn bounds(&self, origin: gpui::Point) -> Bounds { 848 | match self.kind { 849 | CursorKind::Bar => Bounds { 850 | origin: self.origin + origin, 851 | size: size(px(2.0), self.line_height), 852 | }, 853 | CursorKind::Block => Bounds { 854 | origin: self.origin + origin, 855 | size: size(self.block_width, self.line_height), 856 | }, 857 | CursorKind::Underline => Bounds { 858 | origin: self.origin 859 | + origin 860 | + gpui::Point::new(Pixels::ZERO, self.line_height - px(2.0)), 861 | size: size(self.block_width, px(2.0)), 862 | }, 863 | CursorKind::Hidden => todo!(), 864 | } 865 | } 866 | 867 | pub fn paint(&mut self, origin: gpui::Point, cx: &mut WindowContext) { 868 | let bounds = self.bounds(origin); 869 | 870 | let cursor = fill(bounds, self.color); 871 | 872 | cx.paint_quad(cursor); 873 | 874 | if let Some(text) = &self.text { 875 | text.paint(self.origin + origin, self.line_height, cx) 876 | .unwrap(); 877 | } 878 | } 879 | } 880 | 881 | type GutterDecoration<'a, T> = Box; 882 | 883 | trait GutterRenderer { 884 | fn render( 885 | &mut self, 886 | x: u16, 887 | y: u16, 888 | width: usize, 889 | style: helix_view::graphics::Style, 890 | text: Option<&str>, 891 | ); 892 | } 893 | 894 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 895 | struct LinePos { 896 | /// Indicates whether the given visual line 897 | /// is the first visual line of the given document line 898 | pub first_visual_line: bool, 899 | /// The line index of the document line that contains the given visual line 900 | pub doc_line: usize, 901 | /// Vertical offset from the top of the inner view area 902 | pub visual_line: u16, 903 | /// The first char index of this visual line. 904 | /// Note that if the visual line is entirely filled by 905 | /// a very long inline virtual text then this index will point 906 | /// at the next (non-virtual) char after this visual line 907 | pub start_char_idx: usize, 908 | } 909 | 910 | // TODO: copy-pasted from helix_term ui/document.rs 911 | 912 | /// A wrapper around a HighlightIterator 913 | /// that merges the layered highlights to create the final text style 914 | /// and yields the active text style and the char_idx where the active 915 | /// style will have to be recomputed. 916 | struct StyleIter<'a, H: Iterator> { 917 | text_style: helix_view::graphics::Style, 918 | active_highlights: Vec, 919 | highlight_iter: H, 920 | theme: &'a Theme, 921 | } 922 | 923 | impl> Iterator for StyleIter<'_, H> { 924 | type Item = (helix_view::graphics::Style, usize, usize); 925 | 926 | fn next(&mut self) -> Option<(helix_view::graphics::Style, usize, usize)> { 927 | while let Some(event) = self.highlight_iter.next() { 928 | match event { 929 | HighlightEvent::HighlightStart(highlights) => { 930 | self.active_highlights.push(highlights) 931 | } 932 | HighlightEvent::HighlightEnd => { 933 | self.active_highlights.pop(); 934 | } 935 | HighlightEvent::Source { start, end } => { 936 | if start == end { 937 | continue; 938 | } 939 | let style = self 940 | .active_highlights 941 | .iter() 942 | .fold(self.text_style, |acc, span| { 943 | acc.patch(self.theme.highlight(span.0)) 944 | }); 945 | return Some((style, start, end)); 946 | } 947 | } 948 | } 949 | None 950 | } 951 | } 952 | 953 | struct DiagnosticView { 954 | diagnostic: Diagnostic, 955 | theme: Theme, 956 | } 957 | 958 | impl Render for DiagnosticView { 959 | fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { 960 | debug!("rendering diag {:?}", self.diagnostic); 961 | 962 | fn color(style: helix_view::graphics::Style) -> Hsla { 963 | style.fg.and_then(color_to_hsla).unwrap_or(white()) 964 | } 965 | 966 | let theme = &self.theme; 967 | let text_style = theme.get("ui.text.info"); 968 | let popup_style = theme.get("ui.popup.info"); 969 | let warning = theme.get("warning"); 970 | let error = theme.get("error"); 971 | let info = theme.get("info"); 972 | let hint = theme.get("hint"); 973 | 974 | let fg = text_style.fg.and_then(color_to_hsla).unwrap_or(white()); 975 | let bg = popup_style.bg.and_then(color_to_hsla).unwrap_or(black()); 976 | 977 | let title_color = match self.diagnostic.severity { 978 | Some(DiagnosticSeverity::WARNING) => color(warning), 979 | Some(DiagnosticSeverity::ERROR) => color(error), 980 | Some(DiagnosticSeverity::INFORMATION) => color(info), 981 | Some(DiagnosticSeverity::HINT) => color(hint), 982 | _ => fg, 983 | }; 984 | 985 | let font = cx.global::().fixed_font.clone(); 986 | let source_and_code = self.diagnostic.source.as_ref().and_then(|src| { 987 | let code = self.diagnostic.code.as_ref(); 988 | let code_str = code.map(|code| match code { 989 | NumberOrString::Number(num) => num.to_string(), 990 | NumberOrString::String(str) => str.to_string(), 991 | }); 992 | Some(format!("{}: {}", src, code_str.unwrap_or_default())) 993 | }); 994 | 995 | div() 996 | .p_2() 997 | .gap_2() 998 | .shadow_sm() 999 | .rounded_sm() 1000 | .bg(black()) 1001 | .flex() 1002 | .flex_col() 1003 | .font(font) 1004 | .text_size(px(12.)) 1005 | .text_color(fg) 1006 | .bg(bg) 1007 | .child( 1008 | div() 1009 | .flex() 1010 | .font_weight(FontWeight::BOLD) 1011 | .text_color(title_color) 1012 | .justify_center() 1013 | .items_center() 1014 | .when_some(source_and_code, |this, source| this.child(source.clone())), 1015 | ) 1016 | .child(div().flex_col().child(self.diagnostic.message.clone())) 1017 | } 1018 | } 1019 | -------------------------------------------------------------------------------- /src/info_box.rs: -------------------------------------------------------------------------------- 1 | use gpui::prelude::FluentBuilder; 2 | use gpui::*; 3 | use helix_view::info::Info; 4 | 5 | #[derive(Debug)] 6 | pub struct InfoBoxView { 7 | title: Option, 8 | text: Option, 9 | style: Style, 10 | focus: FocusHandle, 11 | } 12 | 13 | impl InfoBoxView { 14 | pub fn new(style: Style, focus: &FocusHandle) -> Self { 15 | InfoBoxView { 16 | title: None, 17 | text: None, 18 | style, 19 | focus: focus.clone(), 20 | } 21 | } 22 | 23 | fn handle_event(&mut self, ev: &crate::Update, cx: &mut ViewContext) { 24 | if let crate::Update::Info(info) = ev { 25 | self.set_info(info); 26 | cx.notify(); 27 | } 28 | } 29 | 30 | pub fn subscribe(&self, editor: &Model, cx: &mut ViewContext) { 31 | cx.subscribe(editor, |this, _, ev, cx| { 32 | this.handle_event(ev, cx); 33 | }) 34 | .detach() 35 | } 36 | 37 | pub fn is_empty(&self) -> bool { 38 | self.title.is_none() 39 | } 40 | 41 | pub fn set_info(&mut self, info: &Info) { 42 | self.title = Some(info.title.clone().into()); 43 | self.text = Some(info.text.clone().into()); 44 | } 45 | } 46 | 47 | impl FocusableView for InfoBoxView { 48 | fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { 49 | self.focus.clone() 50 | } 51 | } 52 | impl EventEmitter for InfoBoxView {} 53 | 54 | impl Render for InfoBoxView { 55 | fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { 56 | let font = cx.global::().fixed_font.clone(); 57 | 58 | div() 59 | .track_focus(&self.focus) 60 | .on_key_down(cx.listener(|_v, _e, cx| { 61 | println!("INFO BOX received key"); 62 | cx.emit(DismissEvent) 63 | })) 64 | .absolute() 65 | .bottom_7() 66 | .right_1() 67 | .flex() 68 | .flex_row() 69 | .child( 70 | div() 71 | .rounded_sm() 72 | .shadow_sm() 73 | .font(font) 74 | .text_size(px(12.)) 75 | .text_color(self.style.text.color.unwrap()) 76 | .bg(self.style.background.as_ref().cloned().unwrap()) 77 | .p_2() 78 | .flex() 79 | .flex_row() 80 | .content_end() 81 | .when_some(self.title.as_ref(), |this, title| { 82 | this.child( 83 | div() 84 | .flex() 85 | .flex_col() 86 | .child( 87 | div() 88 | .flex() 89 | .font_weight(FontWeight::BOLD) 90 | .flex_none() 91 | .justify_center() 92 | .items_center() 93 | .child(title.clone()), 94 | ) 95 | .when_some(self.text.as_ref(), |this, text| { 96 | this.child(text.clone()) 97 | }), 98 | ) 99 | }), 100 | ) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use anyhow::{Context, Error, Result}; 4 | use helix_core::diagnostic::Severity; 5 | use helix_loader::VERSION_AND_GIT_HASH; 6 | use helix_term::args::Args; 7 | use helix_term::config::{Config, ConfigLoadError}; 8 | 9 | use gpui::{ 10 | actions, App, AppContext, Context as _, Menu, MenuItem, TitlebarOptions, VisualContext as _, 11 | WindowBackgroundAppearance, WindowKind, WindowOptions, 12 | }; 13 | 14 | pub use application::Input; 15 | use application::{Application, InputEvent}; 16 | 17 | mod application; 18 | mod document; 19 | mod info_box; 20 | mod notification; 21 | mod overlay; 22 | mod picker; 23 | mod prompt; 24 | mod statusline; 25 | mod utils; 26 | mod workspace; 27 | 28 | pub type Core = Application; 29 | 30 | fn setup_logging(verbosity: u64) -> Result<()> { 31 | let mut base_config = fern::Dispatch::new(); 32 | 33 | base_config = match verbosity { 34 | 0 => base_config.level(log::LevelFilter::Warn), 35 | 1 => base_config.level(log::LevelFilter::Info), 36 | 2 => base_config.level(log::LevelFilter::Debug), 37 | _3_or_more => base_config.level(log::LevelFilter::Trace), 38 | }; 39 | 40 | // Separate file config so we can include year, month and day in file logs 41 | let file_config = fern::Dispatch::new() 42 | .format(|out, message, record| { 43 | out.finish(format_args!( 44 | "{} {} [{}] {}", 45 | chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f"), 46 | record.target(), 47 | record.level(), 48 | message 49 | )) 50 | }) 51 | .chain(std::io::stdout()) 52 | .chain(fern::log_file(helix_loader::log_file())?); 53 | 54 | base_config.chain(file_config).apply()?; 55 | 56 | Ok(()) 57 | } 58 | 59 | fn main() -> Result<()> { 60 | let rt = tokio::runtime::Runtime::new().unwrap(); 61 | let handle = rt.handle(); 62 | let _guard = handle.enter(); 63 | let app = init_editor().unwrap().unwrap(); 64 | drop(_guard); 65 | gui_main(app, handle.clone()); 66 | Ok(()) 67 | } 68 | 69 | fn window_options(_cx: &mut AppContext) -> gpui::WindowOptions { 70 | WindowOptions { 71 | app_id: Some("helix-gpui".to_string()), 72 | titlebar: Some(TitlebarOptions { 73 | title: None, 74 | appears_transparent: true, 75 | traffic_light_position: None, //Some(point(px(9.0), px(9.0))), 76 | }), 77 | window_bounds: None, 78 | focus: true, 79 | show: true, 80 | kind: WindowKind::Normal, 81 | is_movable: true, 82 | display_id: None, 83 | window_background: WindowBackgroundAppearance::Opaque, 84 | } 85 | } 86 | 87 | actions!( 88 | workspace, 89 | [ 90 | About, 91 | Quit, 92 | ShowModal, 93 | Hide, 94 | HideOthers, 95 | ShowAll, 96 | OpenFile, 97 | Undo, 98 | Redo, 99 | Copy, 100 | Paste, 101 | Minimize, 102 | MinimizeAll, 103 | Zoom, 104 | Tutor 105 | ] 106 | ); 107 | 108 | fn app_menus() -> Vec> { 109 | vec![ 110 | Menu { 111 | name: "Helix", 112 | items: vec![ 113 | MenuItem::action("About", About), 114 | MenuItem::separator(), 115 | // MenuItem::action("Settings", OpenSettings), 116 | // MenuItem::separator(), 117 | MenuItem::action("Hide Helix", Hide), 118 | MenuItem::action("Hide Others", HideOthers), 119 | MenuItem::action("Show All", ShowAll), 120 | MenuItem::action("Quit", Quit), 121 | ], 122 | }, 123 | Menu { 124 | name: "File", 125 | items: vec![ 126 | MenuItem::action("Open...", OpenFile), 127 | // MenuItem::action("Open Directory", OpenDirectory), 128 | ], 129 | }, 130 | Menu { 131 | name: "Edit", 132 | items: vec![ 133 | MenuItem::action("Undo", Undo), 134 | MenuItem::action("Redo", Redo), 135 | MenuItem::separator(), 136 | MenuItem::action("Copy", Copy), 137 | MenuItem::action("Paste", Paste), 138 | ], 139 | }, 140 | Menu { 141 | name: "Window", 142 | items: vec![ 143 | MenuItem::action("Minimize", Minimize), 144 | MenuItem::action("Minimize All", MinimizeAll), 145 | MenuItem::action("Zoom", Zoom), 146 | ], 147 | }, 148 | Menu { 149 | name: "Help", 150 | items: vec![MenuItem::action("Tutorial", Tutor)], 151 | }, 152 | ] 153 | } 154 | 155 | #[derive(Debug, Clone, PartialEq, Eq)] 156 | pub struct EditorStatus { 157 | pub status: String, 158 | pub severity: Severity, 159 | } 160 | 161 | #[derive(Debug)] 162 | pub enum Update { 163 | Redraw, 164 | Prompt(prompt::Prompt), 165 | Picker(picker::Picker), 166 | Info(helix_view::info::Info), 167 | EditorEvent(helix_view::editor::EditorEvent), 168 | EditorStatus(EditorStatus), 169 | } 170 | 171 | impl gpui::EventEmitter for Application {} 172 | 173 | struct FontSettings { 174 | fixed_font: gpui::Font, 175 | var_font: gpui::Font, 176 | } 177 | 178 | impl gpui::Global for FontSettings {} 179 | 180 | fn gui_main(app: Application, handle: tokio::runtime::Handle) { 181 | App::new().run(|cx: &mut AppContext| { 182 | let options = window_options(cx); 183 | 184 | cx.open_window(options, |cx| { 185 | let input = cx.new_model(|_| crate::application::Input); 186 | let crank = cx.new_model(|mc| { 187 | mc.spawn(|crank, mut cx| async move { 188 | loop { 189 | cx.background_executor() 190 | .timer(Duration::from_millis(50)) 191 | .await; 192 | let _ = crank.update(&mut cx, |_crank, cx| { 193 | cx.emit(()); 194 | }); 195 | } 196 | }) 197 | .detach(); 198 | crate::application::Crank 199 | }); 200 | let crank_1 = crank.clone(); 201 | std::mem::forget(crank_1); 202 | 203 | let input_1 = input.clone(); 204 | let handle_1 = handle.clone(); 205 | let app = cx.new_model(move |mc| { 206 | let handle_1 = handle_1.clone(); 207 | let handle_2 = handle_1.clone(); 208 | mc.subscribe( 209 | &input_1.clone(), 210 | move |this: &mut Application, _, ev, cx| { 211 | this.handle_input_event(ev.clone(), cx, handle_1.clone()); 212 | }, 213 | ) 214 | .detach(); 215 | mc.subscribe(&crank, move |this: &mut Application, _, ev, cx| { 216 | this.handle_crank_event(*ev, cx, handle_2.clone()); 217 | }) 218 | .detach(); 219 | app 220 | }); 221 | 222 | cx.activate(true); 223 | cx.set_menus(app_menus()); 224 | 225 | let font_settings = FontSettings { 226 | fixed_font: gpui::font("JetBrains Mono"), 227 | var_font: gpui::font("SF Pro"), 228 | }; 229 | cx.set_global(font_settings); 230 | 231 | let input_1 = input.clone(); 232 | cx.new_view(|cx| { 233 | cx.subscribe(&app, |w: &mut workspace::Workspace, _, ev, cx| { 234 | w.handle_event(ev, cx); 235 | }) 236 | .detach(); 237 | workspace::Workspace::new(app, input_1.clone(), handle, cx) 238 | }) 239 | }); 240 | }) 241 | } 242 | 243 | fn init_editor() -> Result> { 244 | let help = format!( 245 | "\ 246 | {} {} 247 | {} 248 | {} 249 | 250 | USAGE: 251 | hx [FLAGS] [files]... 252 | 253 | ARGS: 254 | ... Sets the input file to use, position can also be specified via file[:row[:col]] 255 | 256 | FLAGS: 257 | -h, --help Prints help information 258 | --tutor Loads the tutorial 259 | --health [CATEGORY] Checks for potential errors in editor setup 260 | CATEGORY can be a language or one of 'clipboard', 'languages' 261 | or 'all'. 'all' is the default if not specified. 262 | -g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml 263 | -c, --config Specifies a file to use for configuration 264 | -v Increases logging verbosity each use for up to 3 times 265 | --log Specifies a file to use for logging 266 | (default file: {}) 267 | -V, --version Prints version information 268 | --vsplit Splits all given files vertically into different windows 269 | --hsplit Splits all given files horizontally into different windows 270 | -w, --working-dir Specify an initial working directory 271 | +N Open the first given file at line number N 272 | ", 273 | env!("CARGO_PKG_NAME"), 274 | VERSION_AND_GIT_HASH, 275 | env!("CARGO_PKG_AUTHORS"), 276 | env!("CARGO_PKG_DESCRIPTION"), 277 | helix_loader::default_log_file().display(), 278 | ); 279 | 280 | let mut args = Args::parse_args().context("could not parse arguments")?; 281 | 282 | helix_loader::initialize_config_file(args.config_file.clone()); 283 | helix_loader::initialize_log_file(args.log_file.clone()); 284 | 285 | // Help has a higher priority and should be handled separately. 286 | if args.display_help { 287 | print!("{}", help); 288 | std::process::exit(0); 289 | } 290 | 291 | if args.display_version { 292 | println!("helix {}", VERSION_AND_GIT_HASH); 293 | std::process::exit(0); 294 | } 295 | 296 | if args.health { 297 | if let Err(err) = helix_term::health::print_health(args.health_arg) { 298 | // Piping to for example `head -10` requires special handling: 299 | // https://stackoverflow.com/a/65760807/7115678 300 | if err.kind() != std::io::ErrorKind::BrokenPipe { 301 | return Err(err.into()); 302 | } 303 | } 304 | 305 | std::process::exit(0); 306 | } 307 | 308 | if args.fetch_grammars { 309 | helix_loader::grammar::fetch_grammars()?; 310 | return Ok(None); 311 | } 312 | 313 | if args.build_grammars { 314 | helix_loader::grammar::build_grammars(None)?; 315 | return Ok(None); 316 | } 317 | 318 | setup_logging(args.verbosity).context("failed to initialize logging")?; 319 | 320 | // Before setting the working directory, resolve all the paths in args.files 321 | for (path, _) in args.files.iter_mut() { 322 | *path = helix_stdx::path::canonicalize(&path); 323 | } 324 | 325 | // NOTE: Set the working directory early so the correct configuration is loaded. Be aware that 326 | // Application::new() depends on this logic so it must be updated if this changes. 327 | if let Some(path) = &args.working_directory { 328 | helix_stdx::env::set_current_working_dir(path)?; 329 | } else if let Some((path, _)) = args.files.first().filter(|p| p.0.is_dir()) { 330 | // If the first file is a directory, it will be the working directory unless -w was specified 331 | helix_stdx::env::set_current_working_dir(path)?; 332 | } 333 | 334 | let config = match Config::load_default() { 335 | Ok(config) => config, 336 | Err(ConfigLoadError::Error(err)) if err.kind() == std::io::ErrorKind::NotFound => { 337 | Config::default() 338 | } 339 | Err(ConfigLoadError::Error(err)) => return Err(Error::new(err)), 340 | Err(ConfigLoadError::BadConfig(err)) => { 341 | eprintln!("Bad config: {}", err); 342 | eprintln!("Press to continue with default config"); 343 | use std::io::Read; 344 | let _ = std::io::stdin().read(&mut []); 345 | Config::default() 346 | } 347 | }; 348 | 349 | let lang_loader = helix_core::config::user_lang_loader().unwrap_or_else(|err| { 350 | eprintln!("{}", err); 351 | eprintln!("Press to continue with default language config"); 352 | use std::io::Read; 353 | // This waits for an enter press. 354 | let _ = std::io::stdin().read(&mut []); 355 | helix_core::config::default_lang_loader() 356 | }); 357 | 358 | // TODO: use the thread local executor to spawn the application task separately from the work pool 359 | let app = application::init_editor(args, config, lang_loader) 360 | .context("unable to create new application")?; 361 | 362 | Ok(Some(app)) 363 | } 364 | -------------------------------------------------------------------------------- /src/notification.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::EditorStatus; 4 | use gpui::{prelude::FluentBuilder, *}; 5 | use helix_lsp::{ 6 | lsp::{NumberOrString, ProgressParamsValue, WorkDoneProgress}, 7 | LanguageServerId, 8 | }; 9 | use helix_view::document::DocumentSavedEvent; 10 | use log::info; 11 | 12 | enum LspStatusEvent { 13 | Begin, 14 | Progress, 15 | End, 16 | Ignore, 17 | } 18 | 19 | #[derive(Default, Debug)] 20 | struct LspStatus { 21 | token: String, 22 | title: String, 23 | message: Option, 24 | percentage: Option, 25 | } 26 | 27 | impl LspStatus { 28 | fn is_empty(&self) -> bool { 29 | self.token == "" && self.title == "" && self.message.is_none() 30 | } 31 | } 32 | 33 | #[derive(IntoElement)] 34 | struct Notification { 35 | title: String, 36 | message: Option, 37 | bg: Hsla, 38 | text: Hsla, 39 | } 40 | 41 | impl Notification { 42 | fn from_save_event(event: &Result, bg: Hsla, text: Hsla) -> Self { 43 | let (title, message) = match event { 44 | Ok(saved) => ( 45 | "Saved".to_string(), 46 | format!("saved to {}", saved.path.display()), 47 | ), 48 | Err(err) => ("Error".to_string(), format!("error saving: {}", err)), 49 | }; 50 | 51 | Notification { 52 | title, 53 | message: Some(message), 54 | bg, 55 | text, 56 | } 57 | } 58 | 59 | fn from_editor_status(status: &EditorStatus, bg: Hsla, text: Hsla) -> Self { 60 | use helix_core::diagnostic::Severity; 61 | let title = match status.severity { 62 | Severity::Info => "info", 63 | Severity::Hint => "hint", 64 | Severity::Error => "error", 65 | Severity::Warning => "warning", 66 | } 67 | .to_string(); 68 | 69 | Notification { 70 | title, 71 | message: Some(status.status.clone()), 72 | bg, 73 | text, 74 | } 75 | } 76 | 77 | fn from_lsp(status: &LspStatus, bg: Hsla, text: Hsla) -> Self { 78 | let title = format!( 79 | "{}: {} {}", 80 | status.token, 81 | status.title, 82 | status 83 | .percentage 84 | .map(|s| format!("{}%", s)) 85 | .unwrap_or_default() 86 | ); 87 | Notification { 88 | title, 89 | message: status.message.clone(), 90 | bg, 91 | text, 92 | } 93 | } 94 | } 95 | 96 | pub struct NotificationView { 97 | lsp_status: HashMap, 98 | editor_status: Option, 99 | saved: Option>, 100 | popup_bg_color: Hsla, 101 | popup_text_color: Hsla, 102 | } 103 | 104 | impl NotificationView { 105 | pub fn new(popup_bg_color: Hsla, popup_text_color: Hsla) -> Self { 106 | Self { 107 | saved: None, 108 | editor_status: None, 109 | lsp_status: HashMap::new(), 110 | popup_bg_color, 111 | popup_text_color, 112 | } 113 | } 114 | 115 | fn handle_lsp_call(&mut self, id: LanguageServerId, call: &helix_lsp::Call) -> LspStatusEvent { 116 | use helix_lsp::{Call, Notification}; 117 | let mut ev = LspStatusEvent::Ignore; 118 | 119 | let status = self.lsp_status.entry(id).or_default(); 120 | 121 | match call { 122 | Call::Notification(notification) => { 123 | if let Ok(notification) = 124 | Notification::parse(¬ification.method, notification.params.clone()) 125 | { 126 | match notification { 127 | Notification::ProgressMessage(ref msg) => { 128 | let token = match msg.token.clone() { 129 | NumberOrString::String(s) => s, 130 | NumberOrString::Number(num) => num.to_string(), 131 | }; 132 | status.token = token; 133 | let ProgressParamsValue::WorkDone(value) = msg.value.clone(); 134 | match value { 135 | WorkDoneProgress::Begin(begin) => { 136 | status.title = begin.title; 137 | status.message = begin.message; 138 | status.percentage = begin.percentage; 139 | ev = LspStatusEvent::Begin; 140 | } 141 | WorkDoneProgress::Report(report) => { 142 | if let Some(msg) = report.message { 143 | status.message = Some(msg); 144 | } 145 | status.percentage = report.percentage; 146 | 147 | ev = LspStatusEvent::Progress; 148 | } 149 | WorkDoneProgress::End(end) => { 150 | if let Some(msg) = end.message { 151 | status.message = Some(msg); 152 | } 153 | ev = LspStatusEvent::End; 154 | } 155 | } 156 | } 157 | _ => {} 158 | } 159 | } 160 | } 161 | _ => {} 162 | } 163 | // println!("{:?}", status); 164 | ev 165 | } 166 | 167 | pub fn subscribe(&self, editor: &Model, cx: &mut ViewContext) { 168 | cx.subscribe(editor, |this, _, ev, cx| { 169 | this.handle_event(ev, cx); 170 | }) 171 | .detach() 172 | } 173 | 174 | fn handle_event(&mut self, ev: &crate::Update, cx: &mut ViewContext) { 175 | use helix_view::editor::EditorEvent; 176 | 177 | info!("handling event {:?}", ev); 178 | if let crate::Update::EditorStatus(status) = ev { 179 | self.editor_status = Some(status.clone()); 180 | cx.notify(); 181 | } 182 | if let crate::Update::EditorEvent(EditorEvent::DocumentSaved(ev)) = ev { 183 | self.saved = Some(ev.as_ref().map_err(|e| e.to_string()).map(|ok| ok.clone())); 184 | cx.notify(); 185 | } 186 | if let crate::Update::EditorEvent(EditorEvent::LanguageServerMessage((id, call))) = ev { 187 | let ev = self.handle_lsp_call(*id, call); 188 | match ev { 189 | LspStatusEvent::Begin => { 190 | let id = *id; 191 | cx.spawn(|this, mut cx| async move { 192 | loop { 193 | cx.background_executor() 194 | .timer(std::time::Duration::from_millis(5000)) 195 | .await; 196 | this.update(&mut cx, |this, _cx| { 197 | if this.lsp_status.contains_key(&id) { 198 | // TODO: this call causes workspace redraw for some reason 199 | //cx.notify(); 200 | } 201 | }) 202 | .ok(); 203 | } 204 | }) 205 | .detach(); 206 | } 207 | LspStatusEvent::Progress => {} 208 | LspStatusEvent::Ignore => {} 209 | LspStatusEvent::End => { 210 | self.lsp_status.remove(id); 211 | } 212 | } 213 | } 214 | } 215 | } 216 | 217 | impl Render for NotificationView { 218 | fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { 219 | let mut notifications = vec![]; 220 | for status in self.lsp_status.values() { 221 | if status.is_empty() { 222 | continue; 223 | } 224 | notifications.push(Notification::from_lsp( 225 | status, 226 | self.popup_bg_color, 227 | self.popup_text_color, 228 | )); 229 | } 230 | if let Some(status) = &self.editor_status { 231 | notifications.push(Notification::from_editor_status( 232 | &status, 233 | self.popup_bg_color, 234 | self.popup_text_color, 235 | )); 236 | } 237 | if let Some(saved) = self.saved.take() { 238 | notifications.push(Notification::from_save_event( 239 | &saved, 240 | self.popup_bg_color, 241 | self.popup_text_color, 242 | )); 243 | } 244 | div() 245 | .absolute() 246 | .w(DefiniteLength::Fraction(0.33)) 247 | .top_8() 248 | .right_5() 249 | .flex_col() 250 | .gap_8() 251 | .justify_start() 252 | .items_center() 253 | .children(notifications) 254 | } 255 | } 256 | 257 | impl RenderOnce for Notification { 258 | fn render(mut self, cx: &mut WindowContext) -> impl IntoElement { 259 | let message = self.message.take(); 260 | div() 261 | .flex() 262 | .flex_col() 263 | .flex() 264 | .p_2() 265 | .gap_4() 266 | .min_h(px(100.)) 267 | .bg(self.bg) 268 | .text_color(self.text) 269 | .shadow_sm() 270 | .rounded_sm() 271 | .font(cx.global::().fixed_font.clone()) 272 | .text_size(px(12.)) 273 | .child( 274 | div() 275 | .flex() 276 | .font_weight(FontWeight::BOLD) 277 | .flex_none() 278 | .justify_center() 279 | .items_center() 280 | .child(self.title), 281 | ) 282 | .when_some(message, |this, msg| this.child(msg)) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /src/overlay.rs: -------------------------------------------------------------------------------- 1 | use gpui::prelude::FluentBuilder; 2 | use gpui::*; 3 | 4 | use crate::picker::{Picker, PickerElement}; 5 | use crate::prompt::{Prompt, PromptElement}; 6 | 7 | pub struct OverlayView { 8 | prompt: Option, 9 | picker: Option, 10 | focus: FocusHandle, 11 | } 12 | 13 | impl OverlayView { 14 | pub fn new(focus: &FocusHandle) -> Self { 15 | Self { 16 | prompt: None, 17 | picker: None, 18 | focus: focus.clone(), 19 | } 20 | } 21 | 22 | pub fn is_empty(&self) -> bool { 23 | self.prompt.is_none() && self.picker.is_none() 24 | } 25 | 26 | pub fn subscribe(&self, editor: &Model, cx: &mut ViewContext) { 27 | cx.subscribe(editor, |this, _, ev, cx| { 28 | this.handle_event(ev, cx); 29 | }) 30 | .detach() 31 | } 32 | 33 | fn handle_event(&mut self, ev: &crate::Update, cx: &mut ViewContext) { 34 | match ev { 35 | crate::Update::Prompt(prompt) => { 36 | self.prompt = Some(prompt.clone()); 37 | cx.notify(); 38 | } 39 | crate::Update::Picker(picker) => { 40 | self.picker = Some(picker.clone()); 41 | cx.notify(); 42 | } 43 | _ => {} 44 | } 45 | } 46 | } 47 | 48 | impl FocusableView for OverlayView { 49 | fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { 50 | self.focus.clone() 51 | } 52 | } 53 | impl EventEmitter for OverlayView {} 54 | 55 | impl Render for OverlayView { 56 | fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { 57 | println!("rendering overlay"); 58 | div().absolute().size_full().bottom_0().left_0().child( 59 | div() 60 | .flex() 61 | .h_full() 62 | .justify_center() 63 | .items_center() 64 | .when_some(self.prompt.take(), |this, prompt| { 65 | let handle = cx.focus_handle(); 66 | let prompt = PromptElement { 67 | prompt, 68 | focus: handle.clone(), 69 | }; 70 | handle.focus(cx); 71 | this.child(prompt) 72 | }) 73 | .when_some(self.picker.take(), |this, picker| { 74 | let handle = cx.focus_handle(); 75 | let picker = PickerElement { 76 | picker, 77 | focus: handle.clone(), 78 | }; 79 | handle.focus(cx); 80 | this.child(picker) 81 | }), 82 | ) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/picker.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | 3 | use crate::utils::TextWithStyle; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Picker(TextWithStyle); 7 | 8 | // TODO: this is copy-paste from Prompt, refactor it later 9 | impl Picker { 10 | pub fn make( 11 | editor: &mut helix_view::Editor, 12 | prompt: &mut helix_term::ui::Picker, 13 | ) -> Self { 14 | use helix_term::compositor::Component; 15 | let area = editor.tree.area(); 16 | let compositor_rect = helix_view::graphics::Rect { 17 | x: 0, 18 | y: 0, 19 | width: area.width * 2 / 3, 20 | height: area.height, 21 | }; 22 | 23 | let mut comp_ctx = helix_term::compositor::Context { 24 | editor, 25 | scroll: None, 26 | jobs: &mut helix_term::job::Jobs::new(), 27 | }; 28 | let mut buf = tui::buffer::Buffer::empty(compositor_rect); 29 | prompt.render(compositor_rect, &mut buf, &mut comp_ctx); 30 | Self(TextWithStyle::from_buffer(buf)) 31 | } 32 | } 33 | 34 | #[derive(IntoElement)] 35 | pub struct PickerElement { 36 | pub picker: Picker, 37 | pub focus: FocusHandle, 38 | } 39 | 40 | impl RenderOnce for PickerElement { 41 | fn render(self, cx: &mut WindowContext) -> impl IntoElement { 42 | let bg_color = self 43 | .picker 44 | .0 45 | .style(0) 46 | .and_then(|style| style.background_color); 47 | let mut default_style = TextStyle::default(); 48 | default_style.font_family = "JetBrains Mono".into(); 49 | default_style.font_size = px(12.).into(); 50 | default_style.background_color = bg_color; 51 | 52 | // println!("picker: {:?}", self.picker.0); 53 | let text = self.picker.0.into_styled_text(&default_style); 54 | cx.focus(&self.focus); 55 | div() 56 | .track_focus(&self.focus) 57 | .flex() 58 | .flex_col() 59 | .bg(bg_color.unwrap_or(black())) 60 | .shadow_sm() 61 | .rounded_sm() 62 | .text_color(hsla(1., 1., 1., 1.)) 63 | .font(cx.global::().fixed_font.clone()) 64 | .text_size(px(12.)) 65 | .line_height(px(1.3) * px(12.)) 66 | .child(text) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/prompt.rs: -------------------------------------------------------------------------------- 1 | use gpui::*; 2 | 3 | use crate::utils::TextWithStyle; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct Prompt(TextWithStyle); 7 | 8 | impl Prompt { 9 | pub fn make(editor: &mut helix_view::Editor, prompt: &helix_term::ui::Prompt) -> Prompt { 10 | let area = editor.tree.area(); 11 | let compositor_rect = helix_view::graphics::Rect { 12 | x: 0, 13 | y: 0, 14 | width: area.width * 2 / 3, 15 | height: area.height, 16 | }; 17 | 18 | let mut comp_ctx = helix_term::compositor::Context { 19 | editor, 20 | scroll: None, 21 | jobs: &mut helix_term::job::Jobs::new(), 22 | }; 23 | let mut buf = tui::buffer::Buffer::empty(compositor_rect); 24 | prompt.render_prompt(compositor_rect, &mut buf, &mut comp_ctx); 25 | Prompt(TextWithStyle::from_buffer(buf)) 26 | } 27 | } 28 | 29 | #[derive(IntoElement)] 30 | pub struct PromptElement { 31 | pub prompt: Prompt, 32 | pub focus: FocusHandle, 33 | } 34 | 35 | impl RenderOnce for PromptElement { 36 | fn render(self, cx: &mut WindowContext) -> impl IntoElement { 37 | let bg_color = self 38 | .prompt 39 | .0 40 | .style(0) 41 | .and_then(|style| style.background_color); 42 | let mut default_style = TextStyle::default(); 43 | default_style.font_family = "JetBrains Mono".into(); 44 | default_style.font_size = px(12.).into(); 45 | default_style.background_color = bg_color; 46 | 47 | let text = self.prompt.0.into_styled_text(&default_style); 48 | cx.focus(&self.focus); 49 | div() 50 | .track_focus(&self.focus) 51 | .flex() 52 | .flex_col() 53 | .p_5() 54 | .bg(bg_color.unwrap_or(black())) 55 | .shadow_sm() 56 | .rounded_sm() 57 | .text_color(hsla(1., 1., 1., 1.)) 58 | .font(cx.global::().fixed_font.clone()) 59 | .text_size(px(12.)) 60 | .line_height(px(1.3) * px(12.)) 61 | .child(text) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/statusline.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::color_to_hsla; 2 | use crate::Core; 3 | use gpui::*; 4 | use helix_view::{DocumentId, ViewId}; 5 | 6 | #[derive(IntoElement)] 7 | pub struct StatusLine { 8 | core: Model, 9 | doc_id: DocumentId, 10 | view_id: ViewId, 11 | focused: bool, 12 | style: TextStyle, 13 | } 14 | 15 | impl StatusLine { 16 | pub fn new( 17 | core: Model, 18 | doc_id: DocumentId, 19 | view_id: ViewId, 20 | focused: bool, 21 | style: TextStyle, 22 | ) -> Self { 23 | Self { 24 | core, 25 | doc_id, 26 | view_id, 27 | focused, 28 | style, 29 | } 30 | } 31 | 32 | fn style(&self, cx: &mut WindowContext<'_>) -> (Hsla, Hsla) { 33 | let editor = &self.core.read(cx).editor; 34 | let base_style = if self.focused { 35 | editor.theme.get("ui.statusline") 36 | } else { 37 | editor.theme.get("ui.statusline.inactive") 38 | }; 39 | let base_fg = base_style 40 | .fg 41 | .and_then(color_to_hsla) 42 | .unwrap_or(hsla(0.5, 0.5, 0.5, 1.)); 43 | let base_bg = base_style 44 | .bg 45 | .and_then(color_to_hsla) 46 | .unwrap_or(hsla(0.5, 0.5, 0.5, 1.)); 47 | (base_fg, base_bg) 48 | } 49 | 50 | fn text( 51 | &self, 52 | cx: &mut WindowContext<'_>, 53 | base_fg: Hsla, 54 | base_bg: Hsla, 55 | ) -> (StyledText, StyledText, StyledText) { 56 | use self::copy_pasta::{render_status_parts, RenderContext}; 57 | let editor = &self.core.read(cx).editor; 58 | let doc = editor.document(self.doc_id).unwrap(); 59 | let view = editor.tree.get(self.view_id); 60 | 61 | let mut ctx = RenderContext { 62 | editor: &editor, 63 | doc, 64 | view, 65 | focused: self.focused, 66 | }; 67 | 68 | let parts = render_status_parts(&mut ctx); 69 | 70 | let styled = |spans: Vec>| { 71 | let mut text = String::new(); 72 | let mut runs = Vec::new(); 73 | let mut idx = 0; 74 | for span in spans { 75 | let len = span.content.len(); 76 | text.push_str(&span.content); 77 | let fg = span.style.fg.and_then(color_to_hsla).unwrap_or(base_fg); 78 | let bg = span.style.bg.and_then(color_to_hsla).unwrap_or(base_bg); 79 | let mut run = HighlightStyle::default(); 80 | run.color = Some(fg); 81 | run.background_color = Some(bg); 82 | runs.push(((idx..idx + len), run)); 83 | idx += len; 84 | } 85 | StyledText::new(text).with_highlights(&self.style, runs) 86 | }; 87 | 88 | ( 89 | styled(parts.left), 90 | styled(parts.center), 91 | styled(parts.right), 92 | ) 93 | } 94 | } 95 | 96 | impl RenderOnce for StatusLine { 97 | fn render(self, cx: &mut WindowContext<'_>) -> impl IntoElement { 98 | let (base_fg, base_bg) = self.style(cx); 99 | let parts = self.text(cx, base_fg, base_bg); 100 | let (left, center, right) = parts; 101 | 102 | div() 103 | .w_full() 104 | .flex() 105 | .flex_row() 106 | .bg(base_bg) 107 | .justify_between() 108 | .content_stretch() 109 | .text_size(self.style.font_size) 110 | .child( 111 | div() 112 | .w_full() 113 | .flex() 114 | .flex_row() 115 | .content_stretch() 116 | .child(left), 117 | ) 118 | .child(div().flex().child(center)) 119 | .child( 120 | div() 121 | .w_full() 122 | .flex() 123 | .flex_row() 124 | .items_end() 125 | .content_stretch() 126 | .justify_end() 127 | .child(right), 128 | ) 129 | // ) 130 | } 131 | } 132 | 133 | // copy/paste from helix term (ui/statusline.rs) going further 134 | mod copy_pasta { 135 | use helix_core::{coords_at_pos, encoding, Position}; 136 | use helix_view::document::DEFAULT_LANGUAGE_NAME; 137 | use helix_view::document::{Mode, SCRATCH_BUFFER_NAME}; 138 | use helix_view::{Document, Editor, View}; 139 | 140 | use helix_lsp::lsp::DiagnosticSeverity; 141 | use helix_view::editor::StatusLineElement as StatusLineElementID; 142 | 143 | use tui::text::{Span, Spans}; 144 | 145 | pub struct RenderContext<'a> { 146 | pub editor: &'a Editor, 147 | pub doc: &'a Document, 148 | pub view: &'a View, 149 | pub focused: bool, 150 | } 151 | 152 | #[derive(Debug)] 153 | pub struct StatusLineElements<'a> { 154 | pub left: Vec>, 155 | pub center: Vec>, 156 | pub right: Vec>, 157 | } 158 | 159 | pub fn render_status_parts<'a>(context: &mut RenderContext) -> StatusLineElements<'a> { 160 | let config = context.editor.config(); 161 | 162 | let element_ids = &config.statusline.left; 163 | let left = element_ids 164 | .iter() 165 | .map(|element_id| get_render_function(*element_id)) 166 | .flat_map(|render| render(context).0) 167 | .collect::>(); 168 | 169 | let element_ids = &config.statusline.center; 170 | let center = element_ids 171 | .iter() 172 | .map(|element_id| get_render_function(*element_id)) 173 | .flat_map(|render| render(context).0) 174 | .collect::>(); 175 | 176 | let element_ids = &config.statusline.right; 177 | let right = element_ids 178 | .iter() 179 | .map(|element_id| get_render_function(*element_id)) 180 | .flat_map(|render| render(context).0) 181 | .collect::>(); 182 | StatusLineElements { 183 | left, 184 | right, 185 | center, 186 | } 187 | } 188 | 189 | fn get_render_function<'a>( 190 | element_id: StatusLineElementID, 191 | ) -> impl Fn(&RenderContext) -> Spans<'a> { 192 | match element_id { 193 | helix_view::editor::StatusLineElement::Mode => render_mode, 194 | helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner, 195 | helix_view::editor::StatusLineElement::FileBaseName => render_file_base_name, 196 | helix_view::editor::StatusLineElement::FileName => render_file_name, 197 | helix_view::editor::StatusLineElement::FileAbsolutePath => render_file_absolute_path, 198 | helix_view::editor::StatusLineElement::FileModificationIndicator => { 199 | render_file_modification_indicator 200 | } 201 | helix_view::editor::StatusLineElement::ReadOnlyIndicator => render_read_only_indicator, 202 | helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding, 203 | helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending, 204 | helix_view::editor::StatusLineElement::FileType => render_file_type, 205 | helix_view::editor::StatusLineElement::Diagnostics => render_diagnostics, 206 | helix_view::editor::StatusLineElement::WorkspaceDiagnostics => { 207 | render_workspace_diagnostics 208 | } 209 | helix_view::editor::StatusLineElement::Selections => render_selections, 210 | helix_view::editor::StatusLineElement::PrimarySelectionLength => { 211 | render_primary_selection_length 212 | } 213 | helix_view::editor::StatusLineElement::Position => render_position, 214 | helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage, 215 | helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers, 216 | helix_view::editor::StatusLineElement::Separator => render_separator, 217 | helix_view::editor::StatusLineElement::Spacer => render_spacer, 218 | helix_view::editor::StatusLineElement::VersionControl => render_version_control, 219 | helix_view::editor::StatusLineElement::Register => render_register, 220 | } 221 | } 222 | 223 | fn render_mode<'a>(context: &RenderContext) -> Spans<'a> { 224 | let visible = context.focused; 225 | let config = context.editor.config(); 226 | let modenames = &config.statusline.mode; 227 | let modename = if visible { 228 | match context.editor.mode() { 229 | Mode::Insert => modenames.insert.clone(), 230 | Mode::Select => modenames.select.clone(), 231 | Mode::Normal => modenames.normal.clone(), 232 | } 233 | } else { 234 | // If not focused, explicitly leave an empty space. 235 | " ".into() 236 | }; 237 | let modename = format!(" {} ", modename); 238 | if visible && config.color_modes { 239 | Span::styled( 240 | modename, 241 | match context.editor.mode() { 242 | Mode::Insert => context.editor.theme.get("ui.statusline.insert"), 243 | Mode::Select => context.editor.theme.get("ui.statusline.select"), 244 | Mode::Normal => context.editor.theme.get("ui.statusline.normal"), 245 | }, 246 | ) 247 | .into() 248 | } else { 249 | Span::raw(modename).into() 250 | } 251 | } 252 | 253 | // TODO think about handling multiple language servers 254 | fn render_lsp_spinner<'a>(context: &RenderContext) -> Spans<'a> { 255 | let _language_server = context.doc.language_servers().next(); 256 | Span::raw( 257 | "".to_string(), // language_server 258 | // .and_then(|srv| { 259 | // context 260 | // .spinners 261 | // .get(srv.id()) 262 | // .and_then(|spinner| spinner.frame()) 263 | // }) 264 | // // Even if there's no spinner; reserve its space to avoid elements frequently shifting. 265 | // .unwrap_or(" ") 266 | // .to_string(), 267 | ) 268 | .into() 269 | } 270 | 271 | fn render_diagnostics<'a>(context: &RenderContext) -> Spans<'a> { 272 | let (warnings, errors) = 273 | context 274 | .doc 275 | .diagnostics() 276 | .iter() 277 | .fold((0, 0), |mut counts, diag| { 278 | use helix_core::diagnostic::Severity; 279 | match diag.severity { 280 | Some(Severity::Warning) => counts.0 += 1, 281 | Some(Severity::Error) | None => counts.1 += 1, 282 | _ => {} 283 | } 284 | counts 285 | }); 286 | 287 | let mut output = Spans::default(); 288 | 289 | if warnings > 0 { 290 | output.0.push(Span::styled( 291 | "●".to_string(), 292 | context.editor.theme.get("warning"), 293 | )); 294 | output.0.push(Span::raw(format!(" {} ", warnings))); 295 | } 296 | 297 | if errors > 0 { 298 | output.0.push(Span::styled( 299 | "●".to_string(), 300 | context.editor.theme.get("error"), 301 | )); 302 | output.0.push(Span::raw(format!(" {} ", errors))); 303 | } 304 | 305 | output 306 | } 307 | 308 | fn render_workspace_diagnostics<'a>(context: &RenderContext) -> Spans<'a> { 309 | let (warnings, errors) = 310 | context 311 | .editor 312 | .diagnostics 313 | .values() 314 | .flatten() 315 | .fold((0, 0), |mut counts, (diag, _)| { 316 | match diag.severity { 317 | Some(DiagnosticSeverity::WARNING) => counts.0 += 1, 318 | Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1, 319 | _ => {} 320 | } 321 | counts 322 | }); 323 | 324 | let mut output = Spans::default(); 325 | 326 | if warnings > 0 || errors > 0 { 327 | output.0.push(Span::raw(" W ")); 328 | } 329 | 330 | if warnings > 0 { 331 | output.0.push(Span::styled( 332 | "●".to_string(), 333 | context.editor.theme.get("warning"), 334 | )); 335 | output.0.push(Span::raw(format!(" {} ", warnings))); 336 | } 337 | 338 | if errors > 0 { 339 | output.0.push(Span::styled( 340 | "●".to_string(), 341 | context.editor.theme.get("error"), 342 | )); 343 | output.0.push(Span::raw(format!(" {} ", errors))); 344 | } 345 | 346 | output 347 | } 348 | 349 | fn render_selections<'a>(context: &RenderContext) -> Spans<'a> { 350 | let count = context.doc.selection(context.view.id).len(); 351 | Span::raw(format!( 352 | " {} sel{} ", 353 | count, 354 | if count == 1 { "" } else { "s" } 355 | )) 356 | .into() 357 | } 358 | 359 | fn render_primary_selection_length<'a>(context: &RenderContext) -> Spans<'a> { 360 | let tot_sel = context.doc.selection(context.view.id).primary().len(); 361 | Span::raw(format!( 362 | " {} char{} ", 363 | tot_sel, 364 | if tot_sel == 1 { "" } else { "s" } 365 | )) 366 | .into() 367 | } 368 | 369 | fn get_position(context: &RenderContext) -> Position { 370 | coords_at_pos( 371 | context.doc.text().slice(..), 372 | context 373 | .doc 374 | .selection(context.view.id) 375 | .primary() 376 | .cursor(context.doc.text().slice(..)), 377 | ) 378 | } 379 | 380 | fn render_position<'a>(context: &RenderContext) -> Spans<'a> { 381 | let position = get_position(context); 382 | Span::raw(format!(" {}:{} ", position.row + 1, position.col + 1)).into() 383 | } 384 | 385 | fn render_total_line_numbers<'a>(context: &RenderContext) -> Spans<'a> { 386 | let total_line_numbers = context.doc.text().len_lines(); 387 | Span::raw(format!(" {} ", total_line_numbers)).into() 388 | } 389 | 390 | fn render_position_percentage<'a>(context: &RenderContext) -> Spans<'a> { 391 | let position = get_position(context); 392 | let maxrows = context.doc.text().len_lines(); 393 | Span::raw(format!("{}%", (position.row + 1) * 100 / maxrows)).into() 394 | } 395 | 396 | fn render_file_encoding<'a>(context: &RenderContext) -> Spans<'a> { 397 | let enc = context.doc.encoding(); 398 | 399 | if enc != encoding::UTF_8 { 400 | Span::raw(format!(" {} ", enc.name())).into() 401 | } else { 402 | Spans::default() 403 | } 404 | } 405 | 406 | fn render_file_line_ending<'a>(context: &RenderContext) -> Spans<'a> { 407 | use helix_core::LineEnding::*; 408 | let line_ending = match context.doc.line_ending { 409 | Crlf => "CRLF", 410 | LF => "LF", 411 | #[cfg(feature = "unicode-lines")] 412 | VT => "VT", // U+000B -- VerticalTab 413 | #[cfg(feature = "unicode-lines")] 414 | FF => "FF", // U+000C -- FormFeed 415 | #[cfg(feature = "unicode-lines")] 416 | CR => "CR", // U+000D -- CarriageReturn 417 | #[cfg(feature = "unicode-lines")] 418 | Nel => "NEL", // U+0085 -- NextLine 419 | #[cfg(feature = "unicode-lines")] 420 | LS => "LS", // U+2028 -- Line Separator 421 | #[cfg(feature = "unicode-lines")] 422 | PS => "PS", // U+2029 -- ParagraphSeparator 423 | }; 424 | 425 | Span::raw(format!(" {} ", line_ending)).into() 426 | } 427 | 428 | fn render_file_type<'a>(context: &RenderContext) -> Spans<'a> { 429 | let file_type = context.doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME); 430 | 431 | Span::raw(format!(" {} ", file_type)).into() 432 | } 433 | 434 | fn render_file_name<'a>(context: &RenderContext) -> Spans<'a> { 435 | let title = { 436 | let rel_path = context.doc.relative_path(); 437 | let path = rel_path 438 | .as_ref() 439 | .map(|p| p.to_string_lossy()) 440 | .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); 441 | format!(" {} ", path) 442 | }; 443 | 444 | Span::raw(title).into() 445 | } 446 | 447 | fn render_file_absolute_path<'a>(context: &RenderContext) -> Spans<'a> { 448 | let title = { 449 | let path = context.doc.path(); 450 | let path = path 451 | .as_ref() 452 | .map(|p| p.to_string_lossy()) 453 | .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); 454 | format!(" {} ", path) 455 | }; 456 | 457 | Span::raw(title).into() 458 | } 459 | 460 | fn render_file_modification_indicator<'a>(context: &RenderContext) -> Spans<'a> { 461 | let title = (if context.doc.is_modified() { 462 | "[+]" 463 | } else { 464 | " " 465 | }) 466 | .to_string(); 467 | 468 | Span::raw(title).into() 469 | } 470 | 471 | fn render_read_only_indicator<'a>(context: &RenderContext) -> Spans<'a> { 472 | let title = if context.doc.readonly { 473 | " [readonly] " 474 | } else { 475 | "" 476 | } 477 | .to_string(); 478 | Span::raw(title).into() 479 | } 480 | 481 | fn render_file_base_name<'a>(context: &RenderContext) -> Spans<'a> { 482 | let title = { 483 | let rel_path = context.doc.relative_path(); 484 | let path = rel_path 485 | .as_ref() 486 | .and_then(|p| p.file_name().map(|s| s.to_string_lossy())) 487 | .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into()); 488 | format!(" {} ", path) 489 | }; 490 | 491 | Span::raw(title).into() 492 | } 493 | 494 | fn render_separator<'a>(context: &RenderContext) -> Spans<'a> { 495 | let sep = &context.editor.config().statusline.separator; 496 | 497 | Span::styled( 498 | sep.to_string(), 499 | context.editor.theme.get("ui.statusline.separator"), 500 | ) 501 | .into() 502 | } 503 | 504 | fn render_spacer<'a>(_context: &RenderContext) -> Spans<'a> { 505 | Span::raw(" ").into() 506 | } 507 | 508 | fn render_version_control<'a>(context: &RenderContext) -> Spans<'a> { 509 | let head = context 510 | .doc 511 | .version_control_head() 512 | .unwrap_or_default() 513 | .to_string(); 514 | 515 | Span::raw(head).into() 516 | } 517 | 518 | fn render_register<'a>(context: &RenderContext) -> Spans<'a> { 519 | if let Some(reg) = context.editor.selected_register { 520 | Span::raw(format!(" reg={} ", reg)).into() 521 | } else { 522 | Spans::default() 523 | } 524 | } 525 | } 526 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use gpui::{rgb, HighlightStyle, Hsla, Keystroke, SharedString, StyledText, TextStyle}; 2 | use tui::buffer::Buffer; 3 | 4 | pub fn color_to_hsla(color: helix_view::graphics::Color) -> Option { 5 | use gpui::{black, blue, green, red, white, yellow}; 6 | use helix_view::graphics::Color; 7 | match color { 8 | Color::White => Some(white()), 9 | Color::Black => Some(black()), 10 | Color::Blue => Some(blue()), 11 | Color::Green => Some(green()), 12 | Color::Red => Some(red()), 13 | Color::Yellow => Some(yellow()), 14 | Color::Rgb(r, g, b) => { 15 | let r = (r as u32) << 16; 16 | let g = (g as u32) << 8; 17 | let b = b as u32; 18 | Some(rgb(r | g | b).into()) 19 | } 20 | Color::Reset => None, 21 | any => todo!("{:?} not implemented", any), 22 | } 23 | } 24 | 25 | pub fn translate_key(ks: &Keystroke) -> helix_view::input::KeyEvent { 26 | use helix_view::keyboard::{KeyCode, KeyModifiers}; 27 | 28 | let mut modifiers = KeyModifiers::NONE; 29 | if ks.modifiers.alt { 30 | modifiers |= KeyModifiers::ALT; 31 | } 32 | if ks.modifiers.control { 33 | modifiers |= KeyModifiers::CONTROL; 34 | } 35 | if ks.modifiers.shift { 36 | modifiers |= KeyModifiers::SHIFT; 37 | } 38 | let key = ks.ime_key.as_ref().unwrap_or(&ks.key); 39 | let code = match key.as_str() { 40 | "backspace" => KeyCode::Backspace, 41 | "enter" => KeyCode::Enter, 42 | "left" => KeyCode::Left, 43 | "right" => KeyCode::Right, 44 | "up" => KeyCode::Up, 45 | "down" => KeyCode::Down, 46 | "tab" => KeyCode::Tab, 47 | "escape" => KeyCode::Esc, 48 | "space" => KeyCode::Char(' '), 49 | /* TODO */ 50 | any => { 51 | let chars: Vec = key.chars().collect(); 52 | if chars.len() == 1 { 53 | KeyCode::Char(chars[0]) 54 | } else { 55 | todo!("{:?} key not implemented yet", any) 56 | } 57 | } 58 | }; 59 | 60 | helix_view::input::KeyEvent { code, modifiers } 61 | } 62 | 63 | /// Handle events by looking them up in `self.keymaps`. Returns None 64 | /// if event was handled (a command was executed or a subkeymap was 65 | /// activated). Only KeymapResult::{NotFound, Cancelled} is returned 66 | /// otherwise. 67 | #[allow(unused)] 68 | pub fn handle_key_result( 69 | mode: helix_view::document::Mode, 70 | cxt: &mut helix_term::commands::Context, 71 | key_result: helix_term::keymap::KeymapResult, 72 | ) -> Option { 73 | use helix_term::events::{OnModeSwitch, PostCommand}; 74 | use helix_term::keymap::KeymapResult; 75 | use helix_view::document::Mode; 76 | 77 | let mut last_mode = mode; 78 | //self.pseudo_pending.extend(self.keymaps.pending()); 79 | //let key_result = keymaps.get(mode, event); 80 | //cxt.editor.autoinfo = keymaps.sticky().map(|node| node.infobox()); 81 | 82 | let mut execute_command = |command: &helix_term::commands::MappableCommand| { 83 | command.execute(cxt); 84 | helix_event::dispatch(PostCommand { command, cx: cxt }); 85 | 86 | let current_mode = cxt.editor.mode(); 87 | if current_mode != last_mode { 88 | helix_event::dispatch(OnModeSwitch { 89 | old_mode: last_mode, 90 | new_mode: current_mode, 91 | cx: cxt, 92 | }); 93 | 94 | // HAXX: if we just entered insert mode from normal, clear key buf 95 | // and record the command that got us into this mode. 96 | if current_mode == Mode::Insert { 97 | // how we entered insert mode is important, and we should track that so 98 | // we can repeat the side effect. 99 | //self.last_insert.0 = command.clone(); 100 | //self.last_insert.1.clear(); 101 | } 102 | } 103 | 104 | last_mode = current_mode; 105 | }; 106 | 107 | match &key_result { 108 | KeymapResult::Matched(command) => { 109 | execute_command(command); 110 | } 111 | KeymapResult::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()), 112 | KeymapResult::MatchedSequence(commands) => { 113 | for command in commands { 114 | execute_command(command); 115 | } 116 | } 117 | KeymapResult::NotFound | KeymapResult::Cancelled(_) => return Some(key_result), 118 | } 119 | None 120 | } 121 | 122 | #[derive(Debug, Clone)] 123 | pub struct TextWithStyle { 124 | text: SharedString, 125 | highlights: Vec<(std::ops::Range, HighlightStyle)>, 126 | } 127 | 128 | impl TextWithStyle { 129 | pub fn from_buffer(buf: Buffer) -> Self { 130 | let mut highlights: Vec<(std::ops::Range, HighlightStyle)> = Vec::new(); 131 | 132 | let mut text = String::new(); 133 | let rect = buf.area; 134 | 135 | for y in 0..rect.height { 136 | let mut line = String::new(); 137 | for x in 0..rect.width { 138 | let cell = &buf[(x, y)]; 139 | let bg = crate::utils::color_to_hsla(cell.bg); 140 | let fg = crate::utils::color_to_hsla(cell.fg); 141 | let new_style = HighlightStyle { 142 | color: fg, 143 | background_color: bg, 144 | ..Default::default() 145 | }; 146 | let length = cell.symbol.len(); 147 | let new_range = if let Some((range, current_highlight)) = highlights.last_mut() { 148 | if &new_style == current_highlight { 149 | range.end += length; 150 | None 151 | } else { 152 | let range = range.end..range.end + length; 153 | Some(range) 154 | } 155 | } else { 156 | let range = 0..length; 157 | Some(range) 158 | }; 159 | if let Some(new_range) = new_range { 160 | highlights.push((new_range, new_style)); 161 | } 162 | line.push_str(&cell.symbol); 163 | } 164 | if line.chars().all(|c| c == ' ') { 165 | let mut hl_is_empty = false; 166 | if let Some(hl) = highlights.last_mut() { 167 | hl.0.end -= line.len(); 168 | hl_is_empty = hl.0.end == hl.0.start; 169 | } 170 | if hl_is_empty { 171 | highlights.pop(); 172 | } 173 | continue; 174 | } else { 175 | text.push_str(&line); 176 | text.push_str("\n"); 177 | if let Some(hl) = highlights.last_mut() { 178 | hl.0.end += 1; // new line 179 | } 180 | } 181 | } 182 | 183 | TextWithStyle { 184 | text: text.into(), 185 | highlights, 186 | } 187 | } 188 | 189 | pub fn into_styled_text(self, default_style: &TextStyle) -> StyledText { 190 | StyledText::new(self.text).with_highlights(default_style, self.highlights) 191 | } 192 | 193 | pub fn style(&self, idx: usize) -> Option<&HighlightStyle> { 194 | self.highlights.get(idx).map(|(_, style)| style) 195 | } 196 | } 197 | 198 | pub fn load_tutor(editor: &mut helix_view::editor::Editor) -> Result<(), anyhow::Error> { 199 | use helix_core::{pos_at_coords, Position, Selection}; 200 | use helix_view::doc_mut; 201 | use helix_view::editor::Action; 202 | use std::path::Path; 203 | 204 | let path = helix_loader::runtime_file(Path::new("tutor")); 205 | // let path = Path::new("./test.rs"); 206 | let doc_id = editor.open(&path, Action::VerticalSplit)?; 207 | let view_id = editor.tree.focus; 208 | let doc = doc_mut!(editor, &doc_id); 209 | let pos = Selection::point(pos_at_coords( 210 | doc.text().slice(..), 211 | Position::new(0, 0), 212 | true, 213 | )); 214 | doc.set_selection(view_id, pos); 215 | 216 | // Unset path to prevent accidentally saving to the original tutor file. 217 | doc_mut!(editor).set_path(None); 218 | 219 | Ok(()) 220 | } 221 | -------------------------------------------------------------------------------- /src/workspace.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | 3 | use gpui::prelude::FluentBuilder; 4 | use gpui::*; 5 | use helix_view::ViewId; 6 | use log::info; 7 | 8 | use crate::document::DocumentView; 9 | use crate::info_box::InfoBoxView; 10 | use crate::notification::NotificationView; 11 | use crate::overlay::OverlayView; 12 | use crate::utils; 13 | use crate::{Core, Input, InputEvent}; 14 | 15 | pub struct Workspace { 16 | core: Model, 17 | input: Model, 18 | focused_view_id: Option, 19 | documents: HashMap>, 20 | handle: tokio::runtime::Handle, 21 | overlay: View, 22 | info: View, 23 | info_hidden: bool, 24 | notifications: View, 25 | } 26 | 27 | impl Workspace { 28 | pub fn new( 29 | core: Model, 30 | input: Model, 31 | handle: tokio::runtime::Handle, 32 | cx: &mut ViewContext, 33 | ) -> Self { 34 | let notifications = Self::init_notifications(&core, cx); 35 | let info = Self::init_info_box(&core, cx); 36 | let overlay = cx.new_view(|cx| { 37 | let view = OverlayView::new(&cx.focus_handle()); 38 | view.subscribe(&core, cx); 39 | view 40 | }); 41 | 42 | Self { 43 | core, 44 | input, 45 | focused_view_id: None, 46 | handle, 47 | overlay, 48 | info, 49 | info_hidden: true, 50 | documents: HashMap::default(), 51 | notifications, 52 | } 53 | } 54 | 55 | fn init_notifications( 56 | editor: &Model, 57 | cx: &mut ViewContext, 58 | ) -> View { 59 | let theme = Self::theme(&editor, cx); 60 | let text_style = theme.get("ui.text.info"); 61 | let popup_style = theme.get("ui.popup.info"); 62 | let popup_bg_color = utils::color_to_hsla(popup_style.bg.unwrap()).unwrap_or(black()); 63 | let popup_text_color = utils::color_to_hsla(text_style.fg.unwrap()).unwrap_or(white()); 64 | 65 | cx.new_view(|cx| { 66 | let view = NotificationView::new(popup_bg_color, popup_text_color); 67 | view.subscribe(&editor, cx); 68 | view 69 | }) 70 | } 71 | 72 | fn init_info_box(editor: &Model, cx: &mut ViewContext) -> View { 73 | let theme = Self::theme(editor, cx); 74 | let text_style = theme.get("ui.text.info"); 75 | let popup_style = theme.get("ui.popup.info"); 76 | let fg = text_style 77 | .fg 78 | .and_then(utils::color_to_hsla) 79 | .unwrap_or(white()); 80 | let bg = popup_style 81 | .bg 82 | .and_then(utils::color_to_hsla) 83 | .unwrap_or(black()); 84 | let mut style = Style::default(); 85 | style.text.color = Some(fg); 86 | style.background = Some(bg.into()); 87 | 88 | let info = cx.new_view(|cx| { 89 | let view = InfoBoxView::new(style, &cx.focus_handle()); 90 | view.subscribe(&editor, cx); 91 | view 92 | }); 93 | cx.subscribe(&info, |v, _e, _evt, cx| { 94 | v.info_hidden = true; 95 | cx.notify(); 96 | }) 97 | .detach(); 98 | info 99 | } 100 | 101 | pub fn theme(editor: &Model, cx: &mut ViewContext) -> helix_view::Theme { 102 | editor.read(cx).editor.theme.clone() 103 | } 104 | 105 | pub fn handle_event(&mut self, ev: &crate::Update, cx: &mut ViewContext) { 106 | info!("handling event {:?}", ev); 107 | match ev { 108 | crate::Update::EditorEvent(ev) => { 109 | use helix_view::editor::EditorEvent; 110 | match ev { 111 | EditorEvent::Redraw => cx.notify(), 112 | EditorEvent::LanguageServerMessage(_) => { /* handled by notifications */ } 113 | _ => { 114 | info!("editor event {:?} not handled", ev); 115 | } 116 | } 117 | } 118 | crate::Update::EditorStatus(_) => {} 119 | crate::Update::Redraw => { 120 | if let Some(view) = self.focused_view_id.and_then(|id| self.documents.get(&id)) { 121 | view.update(cx, |_view, cx| { 122 | cx.notify(); 123 | }) 124 | } 125 | cx.notify(); 126 | } 127 | crate::Update::Prompt(_) | crate::Update::Picker(_) => { 128 | // handled by overlay 129 | cx.notify(); 130 | } 131 | crate::Update::Info(_) => { 132 | self.info_hidden = false; 133 | // handled by the info box view 134 | } 135 | } 136 | } 137 | 138 | fn render_tree( 139 | root_id: ViewId, 140 | root: Div, 141 | containers: &mut HashMap, 142 | tree: &HashMap>, 143 | ) -> Div { 144 | let mut root = root; 145 | if let Some(children) = tree.get(&root_id) { 146 | for child_id in children { 147 | let child = containers.remove(child_id).unwrap(); 148 | let child = Self::render_tree(*child_id, child, containers, tree); 149 | root = root.child(child); 150 | } 151 | } 152 | root 153 | } 154 | 155 | fn handle_key(&mut self, ev: &KeyDownEvent, cx: &mut ViewContext) { 156 | println!("WORKSPACE KEY DOWN: {:?}", ev.keystroke); 157 | 158 | let key = utils::translate_key(&ev.keystroke); 159 | self.input.update(cx, |_, cx| { 160 | cx.emit(InputEvent::Key(key)); 161 | }) 162 | } 163 | 164 | fn make_views( 165 | &mut self, 166 | view_ids: &mut HashSet, 167 | right_borders: &mut HashSet, 168 | cx: &mut ViewContext, 169 | ) -> Option { 170 | let editor = &self.core.read(cx).editor; 171 | let mut focused_file_name = None; 172 | 173 | for (view, is_focused) in editor.tree.views() { 174 | let view_id = view.id; 175 | 176 | if editor 177 | .tree 178 | .find_split_in_direction(view_id, helix_view::tree::Direction::Right) 179 | .is_some() 180 | { 181 | right_borders.insert(view_id); 182 | } 183 | 184 | view_ids.insert(view_id); 185 | 186 | if is_focused { 187 | let doc = editor.document(view.doc).unwrap(); 188 | self.focused_view_id = Some(view_id); 189 | focused_file_name = doc.path().map(|p| p.display().to_string()); 190 | } 191 | } 192 | 193 | for view_id in view_ids.iter() { 194 | let view_id = *view_id; 195 | let is_focused = self.focused_view_id == Some(view_id); 196 | let style = TextStyle { 197 | font_family: cx.global::().fixed_font.family.clone(), 198 | font_size: px(14.0).into(), 199 | ..Default::default() 200 | }; 201 | let core = self.core.clone(); 202 | let input = self.input.clone(); 203 | let view = self.documents.entry(view_id).or_insert_with(|| { 204 | cx.new_view(|cx| { 205 | DocumentView::new( 206 | core, 207 | input, 208 | view_id, 209 | style.clone(), 210 | &cx.focus_handle(), 211 | is_focused, 212 | ) 213 | }) 214 | }); 215 | view.update(cx, |view, _cx| { 216 | view.set_focused(is_focused); 217 | }); 218 | } 219 | focused_file_name 220 | } 221 | } 222 | 223 | impl Render for Workspace { 224 | fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { 225 | let mut view_ids = HashSet::new(); 226 | let mut right_borders = HashSet::new(); 227 | 228 | let focused_file_name = self.make_views(&mut view_ids, &mut right_borders, cx); 229 | 230 | let editor = &self.core.read(cx).editor; 231 | 232 | let default_style = editor.theme.get("ui.background"); 233 | let default_ui_text = editor.theme.get("ui.text"); 234 | let bg_color = utils::color_to_hsla(default_style.bg.unwrap()).unwrap_or(black()); 235 | let text_color = utils::color_to_hsla(default_ui_text.fg.unwrap()).unwrap_or(white()); 236 | let window_style = editor.theme.get("ui.window"); 237 | let border_color = utils::color_to_hsla(window_style.fg.unwrap()).unwrap_or(white()); 238 | 239 | let editor_rect = editor.tree.area(); 240 | 241 | use helix_view::tree::{ContainerItem, Layout}; 242 | let mut containers = HashMap::new(); 243 | let mut tree = HashMap::new(); 244 | let mut root_id = None; 245 | 246 | let editor = &self.core.read(cx).editor; 247 | for item in editor.tree.traverse_containers() { 248 | match item { 249 | ContainerItem::Container { id, parent, layout } => { 250 | let container = match layout { 251 | Layout::Horizontal => div().flex().size_full().flex_col(), 252 | Layout::Vertical => div().flex().size_full().flex_row(), 253 | }; 254 | containers.insert(id, container); 255 | let entry = tree.entry(parent).or_insert_with(|| Vec::new()); 256 | 257 | if id == parent { 258 | root_id = Some(id); 259 | } else { 260 | entry.push(id); 261 | } 262 | } 263 | ContainerItem::Child { id, parent } => { 264 | let view = self.documents.get(&id).unwrap().clone(); 265 | let has_border = right_borders.contains(&id); 266 | let view = div() 267 | .flex() 268 | .size_full() 269 | .child(view) 270 | .when(has_border, |this| { 271 | this.border_color(border_color).border_r_1() 272 | }); 273 | 274 | let mut container = containers.remove(&parent).unwrap(); 275 | container = container.child(view); 276 | containers.insert(parent, container); 277 | } 278 | } 279 | } 280 | 281 | let to_remove = self 282 | .documents 283 | .keys() 284 | .copied() 285 | .filter(|id| !view_ids.contains(id)) 286 | .collect::>(); 287 | for view_id in to_remove { 288 | if let Some(view) = self.documents.remove(&view_id) { 289 | cx.dismiss_view(&view); 290 | } 291 | } 292 | 293 | let mut docs_root = None; 294 | // println!("containers: {:?} tree: {:?}", containers.len(), tree); 295 | if let Some(root_id) = root_id { 296 | let root = containers.remove(&root_id).unwrap(); 297 | let child = Self::render_tree(root_id, root, &mut containers, &tree); 298 | let root = div().flex().w_full().h_full().child(child); 299 | docs_root = Some(root); 300 | } 301 | // docs.push(root); 302 | // for view in self.documents.values() { 303 | // docs.push(AnyView::from(view.clone()).cached(StyleRefinement::default().size_full())); 304 | // } 305 | 306 | let focused_view = self 307 | .focused_view_id 308 | .and_then(|id| self.documents.get(&id)) 309 | .cloned(); 310 | if let Some(view) = &focused_view { 311 | cx.focus_view(view); 312 | } 313 | 314 | let label = if let Some(path) = focused_file_name { 315 | div() 316 | .flex_shrink() 317 | .font(cx.global::().var_font.clone()) 318 | .text_color(text_color) 319 | .text_size(px(12.)) 320 | .child(format!("{} - Helix", path)) 321 | } else { 322 | div().flex() 323 | }; 324 | let top_bar = div() 325 | .w_full() 326 | .flex() 327 | .flex_none() 328 | .h_8() 329 | .justify_center() 330 | .items_center() 331 | .child(label); 332 | 333 | println!("rendering workspace"); 334 | 335 | self.core.update(cx, |core, _cx| { 336 | core.compositor.resize(editor_rect); 337 | }); 338 | 339 | if let Some(view) = &focused_view { 340 | cx.focus_view(view); 341 | } 342 | 343 | div() 344 | .on_key_down(cx.listener(|view, ev, cx| { 345 | view.handle_key(ev, cx); 346 | })) 347 | .on_action(move |&crate::About, _cx| { 348 | eprintln!("hello"); 349 | }) 350 | .on_action({ 351 | let handle = self.handle.clone(); 352 | let core = self.core.clone(); 353 | 354 | move |&crate::Quit, cx| { 355 | eprintln!("quit?"); 356 | quit(core.clone(), handle.clone(), cx); 357 | eprintln!("quit!"); 358 | cx.quit(); 359 | } 360 | }) 361 | .on_action({ 362 | let handle = self.handle.clone(); 363 | let core = self.core.clone(); 364 | 365 | move |&crate::OpenFile, cx| { 366 | info!("open file"); 367 | open(core.clone(), handle.clone(), cx) 368 | } 369 | }) 370 | .on_action(move |&crate::Hide, cx| cx.hide()) 371 | .on_action(move |&crate::HideOthers, cx| cx.hide_other_apps()) 372 | .on_action(move |&crate::ShowAll, cx| cx.unhide_other_apps()) 373 | .on_action(move |&crate::Minimize, cx| cx.minimize_window()) 374 | .on_action(move |&crate::Zoom, cx| cx.zoom_window()) 375 | .on_action({ 376 | let handle = self.handle.clone(); 377 | let core = self.core.clone(); 378 | cx.listener(move |_, &crate::Tutor, cx| { 379 | load_tutor(core.clone(), handle.clone(), cx) 380 | }) 381 | }) 382 | .id("workspace") 383 | .bg(bg_color) 384 | .flex() 385 | .flex_col() 386 | .w_full() 387 | .h_full() 388 | .focusable() 389 | .child(top_bar) 390 | .when_some(docs_root, |this, docs| this.child(docs)) 391 | .child(self.notifications.clone()) 392 | .when(!self.overlay.read(cx).is_empty(), |this| { 393 | let view = &self.overlay; 394 | cx.focus_view(&view); 395 | this.child(view.clone()) 396 | }) 397 | .when( 398 | !self.info_hidden && !self.info.read(cx).is_empty(), 399 | |this| { 400 | let info = &self.info; 401 | cx.focus_view(&info); 402 | this.child(info.clone()) 403 | }, 404 | ) 405 | } 406 | } 407 | 408 | fn load_tutor(core: Model, handle: tokio::runtime::Handle, cx: &mut ViewContext) { 409 | core.update(cx, move |core, cx| { 410 | let _guard = handle.enter(); 411 | let _ = utils::load_tutor(&mut core.editor); 412 | cx.notify() 413 | }) 414 | } 415 | 416 | fn open(core: Model, handle: tokio::runtime::Handle, cx: &mut WindowContext) { 417 | let path = cx.prompt_for_paths(PathPromptOptions { 418 | files: true, 419 | directories: false, 420 | multiple: false, 421 | }); 422 | cx.spawn(move |mut cx| async move { 423 | if let Ok(Some(path)) = path.await { 424 | use helix_view::editor::Action; 425 | // TODO: handle errors 426 | cx.update(move |cx| { 427 | core.update(cx, move |core, _cx| { 428 | let path = &path[0]; 429 | let _guard = handle.enter(); 430 | let editor = &mut core.editor; 431 | editor.open(path, Action::Replace).unwrap(); 432 | }) 433 | }) 434 | .unwrap(); 435 | } 436 | }) 437 | .detach(); 438 | } 439 | 440 | fn quit(core: Model, rt: tokio::runtime::Handle, cx: &mut WindowContext) { 441 | core.update(cx, |core, _cx| { 442 | let editor = &mut core.editor; 443 | let _guard = rt.enter(); 444 | rt.block_on(async { editor.flush_writes().await }).unwrap(); 445 | let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect(); 446 | for view_id in views { 447 | editor.close(view_id); 448 | } 449 | }); 450 | } 451 | --------------------------------------------------------------------------------