├── .gitignore ├── .vscode └── launch.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── build.rs ├── images ├── capture.png └── capture_palette.png ├── rustfmt.toml ├── src ├── commands.rs ├── icon.ico ├── main.rs ├── seticon.rs ├── theme.rs └── widgets │ ├── bottom_panel.rs │ ├── editor_view.rs │ ├── empty.rs │ ├── extension.rs │ ├── mod.rs │ ├── palette_view.rs │ ├── text_buffer │ ├── buffer.rs │ ├── caret.rs │ ├── edit_stack.rs │ ├── file.rs │ ├── mod.rs │ ├── position.rs │ ├── rope_utils.rs │ └── syntax.rs │ └── window.rs └── vscodetheme ├── Cargo.lock ├── Cargo.toml └── src ├── lib.rs └── themes └── mariana.json /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /vscodetheme/target 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug executable 'nonepad'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--bin=nonepad", 15 | "--package=nonepad" 16 | ], 17 | "filter": { 18 | "name": "nonepad", 19 | "kind": "bin" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in executable 'nonepad'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--bin=nonepad", 34 | "--package=nonepad" 35 | ], 36 | "filter": { 37 | "name": "nonepad", 38 | "kind": "bin" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nonepad" 3 | version = "0.1.0" 4 | authors = ["Mathieu PEPONAS "] 5 | edition = "2018" 6 | build = "build.rs" 7 | license = "Apache-2.0" 8 | description = "A lightweight graphical text editor" 9 | categories = ["text-editors"] 10 | repository = "https://github.com/pepone42/nonepad" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [target.'cfg(windows)'.build-dependencies] 15 | winres = "0.1" 16 | 17 | [dependencies.nonepad-vscodetheme] 18 | path = "vscodetheme" 19 | version = "0.1" 20 | 21 | [dependencies] 22 | druid = { version = "0.8.2", features = ["im"] } 23 | ropey = "1.2" 24 | unicode-segmentation = "1.6.0" 25 | chardetng = "0.1.3" 26 | encoding_rs = "0.8.17" 27 | anyhow = "1.0" 28 | uuid = { version = "0.8", features = ["v4"] } 29 | once_cell = "1.8" 30 | syntect = "4.5" 31 | sublime_fuzzy = "0.7.0" 32 | tracing = "0.1" 33 | tracing-subscriber = "0.3" 34 | hotwatch = "0.4" 35 | 36 | [target.'cfg(windows)'.dependencies] 37 | winapi = "0.3" 38 | 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nonepad 2 | A lightweight text editor made with Rust and Druid, made for recreational purpose 3 | 4 | ## Features: 5 | * Multi Cursor 6 | * Syntax Highlight 7 | * Light 8 | * Command palette (Ctrl-Shift-P) 9 | 10 | ![capture](/images/capture.png?raw=true "Capture") 11 | 12 |

13 | 14 | ![capture](/images/capture_palette.png?raw=true "palette") 15 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | #[cfg(windows)] use winres::WindowsResource; 3 | 4 | fn main() -> io::Result<()> { 5 | #[cfg(windows)] { 6 | WindowsResource::new() 7 | // This path can be absolute, or relative to your crate root. 8 | .set_icon_with_id("src/icon.ico","main_icon") 9 | .compile()?; 10 | } 11 | Ok(()) 12 | } -------------------------------------------------------------------------------- /images/capture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepone42/nonepad/586b9cb9fd6d2d32d96ccaf86e069b99ba395282/images/capture.png -------------------------------------------------------------------------------- /images/capture_palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepone42/nonepad/586b9cb9fd6d2d32d96ccaf86e069b99ba395282/images/capture_palette.png -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | -------------------------------------------------------------------------------- /src/commands.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | 3 | use druid::{im::Vector, Event, EventCtx, FileDialogOptions, HotKey, KeyEvent, Selector, SysMods, Application, ClipboardFormat}; 4 | use once_cell::sync::Lazy; 5 | 6 | use crate::widgets::{ 7 | editor_view::EditorView, 8 | item, 9 | text_buffer::{syntax::SYNTAXSET, EditStack}, 10 | window::{NPWindow, NPWindowState}, 11 | DialogResult, Item, PaletteBuilder, PaletteResult, 12 | }; 13 | 14 | const UICOMMAND_CALLBACK: Selector = Selector::new("nonepad.all.uicommand_callback"); 15 | 16 | #[derive(Clone)] 17 | enum UICommandCallback { 18 | Window(fn(&mut NPWindow, &mut EventCtx, &mut NPWindowState) -> bool), 19 | EditView(fn(&mut EditorView, &mut EventCtx, &mut EditStack) -> bool), 20 | } 21 | 22 | struct UICommand { 23 | pub description: String, 24 | pub show_in_palette: bool, 25 | shortcut: Option, 26 | exec: UICommandCallback, 27 | } 28 | 29 | impl UICommand { 30 | fn new(description: &str, show_in_palette: bool, shortcut: Option, exec: UICommandCallback) -> Self { 31 | Self { 32 | description: description.to_owned(), 33 | show_in_palette, 34 | shortcut, 35 | exec, 36 | } 37 | } 38 | 39 | fn matches(&self, event: &KeyEvent) -> bool { 40 | self.shortcut.clone().map(|s| s.matches(event)).unwrap_or(false) 41 | } 42 | } 43 | 44 | struct UICommandSet { 45 | commands: Vec, 46 | } 47 | 48 | impl UICommandSet { 49 | pub fn new() -> Self { 50 | Self { commands: Vec::new() } 51 | } 52 | } 53 | 54 | pub struct CommandSet; 55 | 56 | pub trait UICommandEventHandler { 57 | fn event(&self, ctx: &mut EventCtx, event: &Event, window: &mut W, editor: &mut D); 58 | } 59 | 60 | impl UICommandEventHandler for CommandSet { 61 | fn event(&self, ctx: &mut EventCtx, event: &Event, window: &mut NPWindow, editor: &mut NPWindowState) { 62 | match event { 63 | Event::KeyDown(event) => { 64 | for c in &WINCOMMANDSET.commands { 65 | if c.matches(event.borrow()) { 66 | if let UICommandCallback::Window(c) = c.exec { 67 | c(window, ctx, editor); 68 | ctx.set_handled(); 69 | } 70 | } 71 | } 72 | } 73 | Event::Command(cmd) if cmd.is(UICOMMAND_CALLBACK) => { 74 | if let UICommandCallback::Window(f) = cmd.get_unchecked(UICOMMAND_CALLBACK) { 75 | f(window, ctx, editor); 76 | ctx.set_handled(); 77 | } 78 | } 79 | _ => (), 80 | } 81 | } 82 | } 83 | 84 | impl UICommandEventHandler for CommandSet { 85 | fn event(&self, ctx: &mut EventCtx, event: &Event, window: &mut EditorView, editor: &mut EditStack) { 86 | match event { 87 | Event::KeyDown(event) => { 88 | for c in &VIEWCOMMANDSET.commands { 89 | if c.matches(event.borrow()) { 90 | if let UICommandCallback::EditView(c) = c.exec { 91 | c(window, ctx, editor); 92 | } 93 | } 94 | } 95 | } 96 | Event::Command(cmd) if cmd.is(UICOMMAND_CALLBACK) => { 97 | if let UICommandCallback::EditView(f) = cmd.get_unchecked(UICOMMAND_CALLBACK) { 98 | f(window, ctx, editor); 99 | ctx.set_handled(); 100 | } 101 | } 102 | _ => (), 103 | } 104 | } 105 | } 106 | 107 | fn string_to_hotkey(input: &str) -> Option { 108 | let t: Vec<&str> = input.split('-').collect(); 109 | if t.len() != 2 { 110 | return None; 111 | } 112 | let mods = match t[0] { 113 | "Ctrl" => SysMods::Cmd, 114 | "CtrlShift" => SysMods::CmdShift, 115 | "CtrlAlt" => SysMods::AltCmd, 116 | "Shift" => SysMods::Shift, 117 | "CtrlAltShift" => SysMods::AltCmdShift, 118 | _ => SysMods::None, 119 | }; 120 | #[cfg(target_os = "macos")] 121 | return Some(HotKey::new(mods, t[1])); 122 | #[cfg(not(target_os = "macos"))] 123 | if t[0].contains("Shift") { 124 | Some(HotKey::new(mods, t[1].to_uppercase().as_str())) 125 | } else { 126 | Some(HotKey::new(mods, t[1])) 127 | } 128 | } 129 | 130 | macro_rules! wincmd { 131 | ($commandset:ident = { $($command:ident = ($description:literal,$hotkey:literal, $v:expr, $b:expr));+ $(;)? } ) => { 132 | static $commandset: Lazy = Lazy::new(|| { 133 | let mut v = UICommandSet::new(); 134 | $(v.commands.push(UICommand::new($description, $v,string_to_hotkey($hotkey), UICommandCallback::Window($b) ));)+ 135 | v 136 | }); 137 | }; 138 | 139 | } 140 | 141 | macro_rules! viewcmd { 142 | ($commandset:ident = { $($command:ident = ($description:literal,$hotkey:literal, $v:expr, $b:expr));+ $(;)? } ) => { 143 | static $commandset: Lazy = Lazy::new(|| { 144 | let mut v = UICommandSet::new(); 145 | $(v.commands.push(UICommand::new($description, $v,string_to_hotkey($hotkey), UICommandCallback::EditView($b) ));)+ 146 | v 147 | }); 148 | }; 149 | 150 | } 151 | 152 | wincmd! { 153 | WINCOMMANDSET = { 154 | PLACMD_SHOW_PAL = ("Show command palette","CtrlShift-p", false, 155 | |win, ctx, _data| { 156 | let mut items = Vector::new(); 157 | for c in WINCOMMANDSET.commands.iter().filter(|c| c.show_in_palette) { 158 | items.push_back(Item::new(&c.description, &"")); 159 | } 160 | let viewcmd_start_index = items.len(); 161 | for c in VIEWCOMMANDSET.commands.iter().filter(|c| c.show_in_palette) { 162 | items.push_back(Item::new(&c.description, &"")); 163 | } 164 | win.palette() 165 | .items(items) 166 | .on_select(move |result, ctx, _, _| { 167 | if result.index>=viewcmd_start_index { 168 | if let Some(ui_cmd) = &VIEWCOMMANDSET.commands.iter().filter(|c| c.show_in_palette).nth(result.index - viewcmd_start_index) { 169 | // TODO: Send command to current editor target, not global 170 | ctx.submit_command(UICOMMAND_CALLBACK.with(ui_cmd.exec.clone())); 171 | } 172 | } else { 173 | if let Some(ui_cmd) = &WINCOMMANDSET.commands.iter().filter(|c| c.show_in_palette).nth(result.index) { 174 | ctx.submit_command(UICOMMAND_CALLBACK.with(ui_cmd.exec.clone())); 175 | } 176 | } 177 | }) 178 | .show(ctx); 179 | true 180 | }); 181 | PALCMD_CHANGE_LANGUAGE = ("Change language mode","CtrlShift-l", true, 182 | |window, ctx, _data| { 183 | let languages: Vector = SYNTAXSET.syntaxes().iter().map(|l| Item::new(&l.name,&format!("File extensions : [{}]",l.file_extensions.join(", ")) )).collect(); 184 | window.palette().items(languages) 185 | .title("Set Language mode to") 186 | .on_select( 187 | |result: PaletteResult, _ctx, _win, data| { 188 | data.editor.file.syntax = SYNTAXSET.find_syntax_by_name(&result.name).unwrap(); 189 | } 190 | ).show(ctx); 191 | true 192 | }); 193 | PALCMD_CHANGE_TYPE_TYPE = ("Change indentation","", true, 194 | |window, ctx, _data| { 195 | window.palette().items(item!["Tabs","Spaces"]) 196 | .title("Indent using") 197 | .on_select( 198 | |result: PaletteResult, _ctx, _win, data| { 199 | if result.index == 0 { 200 | data.editor.file.indentation = crate::widgets::text_buffer::Indentation::Tab(4); 201 | } else { 202 | data.editor.file.indentation = crate::widgets::text_buffer::Indentation::Space(4); 203 | } 204 | } 205 | ).show(ctx); 206 | true 207 | }); 208 | PALCMD_OPEN = ("Open","Ctrl-o", true, 209 | |window, ctx, data| { 210 | if data.editor.is_dirty() { 211 | window.dialog().title("Discard unsaved change?").on_select( 212 | |result, ctx, _, _| { 213 | if result == DialogResult::Ok { 214 | let options = FileDialogOptions::new().show_hidden(); 215 | ctx.submit_command(druid::commands::SHOW_OPEN_PANEL.with(options)); 216 | } 217 | } 218 | ).show(ctx); 219 | } else { 220 | let options = FileDialogOptions::new().show_hidden(); 221 | ctx.submit_command(druid::commands::SHOW_OPEN_PANEL.with(options)); 222 | } 223 | true 224 | }); 225 | PALCMD_SAVE = ("Save","Ctrl-s",true, 226 | |_window, ctx, data| { 227 | if data.editor.filename.is_some() { 228 | ctx.submit_command(druid::commands::SAVE_FILE); 229 | } else { 230 | let options = FileDialogOptions::new().show_hidden(); 231 | ctx.submit_command(druid::commands::SHOW_SAVE_PANEL.with(options)) 232 | } 233 | return true; 234 | }); 235 | PALCMD_SAVE_AS = ("Save As","CtrlShift-s",true, 236 | |_window, ctx, _data| { 237 | let options = FileDialogOptions::new().show_hidden(); 238 | ctx.submit_command(druid::commands::SHOW_SAVE_PANEL.with(options)); 239 | return true; 240 | }); 241 | } 242 | } 243 | 244 | viewcmd! { 245 | VIEWCOMMANDSET = { 246 | PALCMD_GOTO_LINE = ("Navigate to line","Ctrl-g", true, 247 | |view, ctx, _|{ 248 | view.palette().title("Navigate to line").on_select(|result,ctx,ev,editor| { 249 | if let Ok(line) = result.name.parse::() { 250 | ev.navigate_to_line(ctx,editor,line.into() ); 251 | } 252 | }).show(ctx); 253 | return true; 254 | }); 255 | SEARCH = ("Search","Ctrl-f", true, 256 | |_, ctx, editor| { 257 | ctx.submit_command(crate::widgets::bottom_panel::SHOW_SEARCH_PANEL.with(editor.main_cursor_selected_text())); 258 | return true; 259 | }); 260 | DUPLICATE_CURSOR_SELECTION = ("Duplicate cursor","Ctrl-d", false, 261 | |_, _, editor| { 262 | editor 263 | .buffer 264 | .duplicate_cursor_from_str(&editor.main_cursor_selected_text()); 265 | return true; 266 | }); 267 | COPY = ("Copy selections to clipboard","Ctrl-c", false, 268 | |_,_,editor| { 269 | Application::global().clipboard().put_string(editor.selected_text()); 270 | return true; 271 | }); 272 | CUT = ("Cut selections to clipboard","Ctrl-x", false, 273 | |_,_,editor| { 274 | Application::global().clipboard().put_string(editor.selected_text()); 275 | editor.delete(); 276 | return true; 277 | }); 278 | PASTE = ("Paste from clipboard","Ctrl-v", false, 279 | |_,_,editor| { 280 | let clipboard = Application::global().clipboard(); 281 | let supported_types = &[ClipboardFormat::TEXT]; 282 | let best_available_type = clipboard.preferred_format(supported_types); 283 | if let Some(format) = best_available_type { 284 | let data = clipboard 285 | .get_format(format) 286 | .expect("I promise not to unwrap in production"); 287 | editor.insert(String::from_utf8_lossy(&data).as_ref()); 288 | } 289 | return true; 290 | }); 291 | UNDO = ("Undo","Ctrl-z", false, 292 | |_,_,editor| { 293 | editor.undo(); 294 | return true; 295 | }); 296 | REDO = ("redo","Ctrl-y", false, 297 | |_,_,editor| { 298 | editor.redo(); 299 | return true; 300 | }); 301 | SELECT_ALL = ("Select all text","Ctrl-a", true, 302 | |_,_,editor| { 303 | editor.select_all(); 304 | return true; 305 | }); 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /src/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pepone42/nonepad/586b9cb9fd6d2d32d96ccaf86e069b99ba395282/src/icon.ico -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // "Hello 😊︎ 😐︎ ☹︎ example" 2 | #![windows_subsystem = "windows"] 3 | 4 | mod commands; 5 | mod seticon; 6 | mod theme; 7 | mod widgets; 8 | 9 | use druid::{piet::Color, AppDelegate, AppLauncher, Command, DelegateCtx, Env, LocalizedString, Target, WindowDesc}; 10 | use druid::{Data, Menu, Size, WindowHandle, WindowId}; 11 | 12 | use seticon::set_icon; 13 | 14 | use theme::Theme; 15 | use widgets::window::NPWindowState; 16 | 17 | #[derive(Debug)] 18 | pub struct Delegate; 19 | impl AppDelegate for Delegate { 20 | fn event( 21 | &mut self, 22 | _ctx: &mut druid::DelegateCtx, 23 | _window_id: druid::WindowId, 24 | event: druid::Event, 25 | _data: &mut NPWindowState, 26 | _env: &Env, 27 | ) -> Option { 28 | Some(event) 29 | } 30 | fn command( 31 | &mut self, 32 | _ctx: &mut DelegateCtx, 33 | _target: Target, 34 | _cmd: &Command, 35 | _data: &mut NPWindowState, 36 | _env: &Env, 37 | ) -> druid::Handled { 38 | druid::Handled::No 39 | } 40 | fn window_added( 41 | &mut self, 42 | id: druid::WindowId, 43 | _handle: WindowHandle, 44 | _data: &mut NPWindowState, 45 | _env: &Env, 46 | _ctx: &mut druid::DelegateCtx, 47 | ) { 48 | set_icon(id); 49 | } 50 | fn window_removed( 51 | &mut self, 52 | _id: druid::WindowId, 53 | _data: &mut NPWindowState, 54 | _env: &Env, 55 | _ctx: &mut druid::DelegateCtx, 56 | ) { 57 | } 58 | } 59 | 60 | #[allow(unused_assignments, unused_mut)] 61 | fn make_menu(_window: Option, _data: &NPWindowState, _env: &Env) -> Menu { 62 | #[cfg(target_os = "macos")] 63 | /// The 'About App' menu item. 64 | pub fn about() -> druid::MenuItem { 65 | druid::MenuItem::new("About NonePad") 66 | .command(druid::commands::SHOW_ABOUT) 67 | } 68 | #[cfg(target_os = "macos")] 69 | /// The 'Quit' menu item. 70 | pub fn quit() -> druid::MenuItem { 71 | druid::MenuItem::new("Quit") 72 | .command(druid::commands::QUIT_APP) 73 | .hotkey(druid::SysMods::Cmd, "q") 74 | } 75 | #[cfg(target_os = "macos")] 76 | { 77 | Menu::empty().entry(Menu::new("menu") 78 | .entry(about()) 79 | .separator() 80 | .entry(quit())) 81 | } 82 | #[cfg(not(target_os = "macos"))] 83 | { 84 | Menu::empty() 85 | } 86 | } 87 | 88 | fn main() -> anyhow::Result<()> { 89 | #[cfg(target_os = "windows")] 90 | { 91 | use winapi::um::wincon::{AttachConsole, ATTACH_PARENT_PROCESS}; 92 | unsafe { 93 | AttachConsole(ATTACH_PARENT_PROCESS); 94 | } 95 | } 96 | 97 | #[cfg(debug_assertions)] 98 | { 99 | let subscriber = tracing_subscriber::FmtSubscriber::builder().with_max_level(tracing::Level::TRACE).finish(); 100 | 101 | tracing::subscriber::set_global_default(subscriber) 102 | .expect("setting default subscriber failed"); 103 | } 104 | 105 | let app_state = if let Some(filename) = std::env::args().nth(1) { 106 | NPWindowState::from_file(filename)? 107 | } else { 108 | NPWindowState::new() 109 | }; 110 | 111 | let win = WindowDesc::new(widgets::window::NPWindow::build()) 112 | .title(LocalizedString::new("NonePad")) 113 | .with_min_size(Size::new(500., 500.)) 114 | .menu(make_menu); 115 | AppLauncher::with_window(win) 116 | .delegate(Delegate) 117 | .configure_env(|env, _| { 118 | let theme = Theme::default(); 119 | env.set( 120 | druid::theme::WINDOW_BACKGROUND_COLOR, 121 | Color::from_hex_str(&theme.vscode.colors.editor_background).unwrap(), 122 | ); 123 | env.set( 124 | druid::theme::BORDER_DARK, 125 | Color::from_hex_str(&theme.vscode.colors.panel_border).unwrap(), 126 | ); 127 | 128 | theme.to_env(env); 129 | }) 130 | .launch(app_state)?; 131 | Ok(()) 132 | } 133 | -------------------------------------------------------------------------------- /src/seticon.rs: -------------------------------------------------------------------------------- 1 | use druid::WindowId; 2 | 3 | #[cfg(windows)] 4 | mod windows { 5 | use std::ffi::OsStr; 6 | use std::iter::once; 7 | use std::os::windows::prelude::OsStrExt; 8 | 9 | use winapi::shared::minwindef::BOOL; 10 | use winapi::shared::minwindef::DWORD; 11 | use winapi::shared::minwindef::FALSE; 12 | use winapi::shared::minwindef::TRUE; 13 | use winapi::um::libloaderapi::GetModuleHandleW; 14 | use winapi::um::processthreadsapi::GetCurrentProcessId; 15 | use winapi::um::winnt::LPCWSTR; 16 | use winapi::um::winuser::EnumWindows; 17 | use winapi::um::winuser::GetWindowThreadProcessId; 18 | use winapi::{ 19 | shared::minwindef::{LPARAM, WPARAM}, 20 | shared::windef::{HICON, HWND}, 21 | um::winuser::{LoadIconW, SendMessageW, ICON_BIG, ICON_SMALL, WM_SETICON}, 22 | }; 23 | 24 | // winres set_icon or set_icon_with_id must be used in the build for this to work 25 | pub unsafe fn set_windows_icon() { 26 | extern "system" fn enum_windows_callback(hwnd: HWND, _l_param: LPARAM) -> BOOL { 27 | let mut wnd_proc_id: DWORD = 0; 28 | unsafe { 29 | GetWindowThreadProcessId(hwnd, &mut wnd_proc_id as *mut DWORD); 30 | if GetCurrentProcessId() != wnd_proc_id { 31 | return TRUE; 32 | } 33 | let icon_name: Vec = OsStr::new( "main_icon" ).encode_wide().chain( once( 0 ) ).collect(); 34 | let hicon = LoadIconW(GetModuleHandleW(0 as LPCWSTR), icon_name.as_ptr());// MAKEINTRESOURCEW(id)); 35 | if hicon == 0 as HICON { 36 | eprintln!("No Icon main_icon in resource"); 37 | } 38 | 39 | SendMessageW(hwnd, WM_SETICON, ICON_SMALL as WPARAM, hicon as LPARAM); 40 | SendMessageW(hwnd, WM_SETICON, ICON_BIG as WPARAM, hicon as LPARAM); 41 | } 42 | FALSE 43 | } 44 | 45 | EnumWindows(Some(enum_windows_callback), 0); 46 | 47 | } 48 | } 49 | #[cfg(windows)] 50 | pub fn set_icon(_win_id: WindowId) { 51 | unsafe {windows::set_windows_icon()} 52 | } 53 | #[cfg(not(windows))] 54 | pub fn set_icon(_win_id: WindowId) { 55 | // todo 56 | } -------------------------------------------------------------------------------- /src/widgets/bottom_panel.rs: -------------------------------------------------------------------------------- 1 | use druid::{ 2 | widget::{Controller, Flex, Label, TextBox, ViewSwitcher}, 3 | Data, Env, Event, EventCtx, KbKey, KeyEvent, Lens, Selector, Widget, WidgetExt, WidgetId, 4 | }; 5 | 6 | use crate::widgets::{EmptyWidget, Extension, PaletteViewState}; 7 | 8 | pub const PANEL_CLOSED: usize = 0x0; 9 | pub const PANEL_SEARCH: usize = 0x1; 10 | 11 | pub const SHOW_SEARCH_PANEL: Selector = Selector::new("nonepad.bottom_panel.show_search"); 12 | pub const SEND_STRING_DATA: Selector = Selector::new("nonepad.all.send_data"); 13 | pub const CLOSE_BOTTOM_PANEL: Selector<()> = Selector::new("nonepad.bottom_panel.close"); 14 | 15 | pub struct BottomPanel {} 16 | 17 | #[derive(Debug, Clone, Data, Lens, Default)] 18 | pub struct BottonPanelState { 19 | pub current: usize, 20 | search_state: SearchState, 21 | panel_state: PaletteViewState, 22 | } 23 | 24 | impl BottonPanelState { 25 | pub fn is_open(&self) -> bool { 26 | self.current != 0 27 | } 28 | } 29 | 30 | pub fn build() -> impl Widget { 31 | let view_switcher = ViewSwitcher::new( 32 | |data: &BottonPanelState, _env| data.current, 33 | |selector, _data, _env| match *selector { 34 | PANEL_CLOSED => Box::new(EmptyWidget::default()), 35 | PANEL_SEARCH => Box::new(build_search_panel().lens(BottonPanelState::search_state)), 36 | _ => unreachable!(), 37 | }, 38 | ); 39 | let panel_id = WidgetId::next(); 40 | view_switcher.with_id(panel_id).controller(BottomPanel {}) 41 | } 42 | 43 | impl> Controller for BottomPanel { 44 | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut BottonPanelState, env: &Env) { 45 | match event { 46 | Event::KeyDown(KeyEvent { key: KbKey::Escape, .. }) => { 47 | data.current = PANEL_CLOSED; 48 | ctx.focus_prev(); 49 | return; 50 | } 51 | Event::Command(cmd) if cmd.is(CLOSE_BOTTOM_PANEL) => { 52 | data.current = PANEL_CLOSED; 53 | ctx.focus_prev(); 54 | return; 55 | } 56 | Event::Command(cmd) if cmd.is(SHOW_SEARCH_PANEL) => { 57 | data.current = PANEL_SEARCH; 58 | let input = cmd.get_unchecked(SHOW_SEARCH_PANEL).clone(); 59 | ctx.submit_command(SEND_STRING_DATA.with(input).to(ctx.widget_id())); 60 | return; 61 | } 62 | _ => (), 63 | } 64 | child.event(ctx, event, data, env) 65 | } 66 | fn lifecycle( 67 | &mut self, 68 | child: &mut W, 69 | ctx: &mut druid::LifeCycleCtx, 70 | event: &druid::LifeCycle, 71 | data: &BottonPanelState, 72 | env: &Env, 73 | ) { 74 | child.lifecycle(ctx, event, data, env); 75 | } 76 | } 77 | 78 | #[derive(Debug, Clone, Data, Lens, Default)] 79 | struct SearchState { 80 | s: String, 81 | } 82 | 83 | fn build_search_panel() -> impl Widget { 84 | Flex::row() 85 | .with_child(Label::new("Search").with_text_size(12.0)) 86 | .with_flex_child( 87 | TextBox::new() 88 | .with_text_size(12.0) 89 | .on_enter(|ctx, data: &mut String, _| { 90 | ctx.submit_command(super::editor_view::REQUEST_NEXT_SEARCH.with(data.clone())) 91 | }) 92 | .focus() 93 | .on_data_received(|ctx, state: &mut String, data: &String, _| { 94 | ctx.request_focus(); 95 | state.clone_from(data); 96 | }) 97 | .lens(SearchState::s) 98 | .expand_width(), 99 | 1.0, 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /src/widgets/empty.rs: -------------------------------------------------------------------------------- 1 | use druid::{Size, Widget}; 2 | 3 | #[derive(Debug,Default)] 4 | pub struct EmptyWidget { 5 | size: Size, 6 | } 7 | 8 | impl Widget for EmptyWidget { 9 | fn event(&mut self, _ctx: &mut druid::EventCtx, _event: &druid::Event, _data: &mut T, _env: &druid::Env) {} 10 | fn lifecycle(&mut self, _ctx: &mut druid::LifeCycleCtx, _event: &druid::LifeCycle, _data: &T, _env: &druid::Env) {} 11 | fn update(&mut self, _ctx: &mut druid::UpdateCtx, _old_data: &T, _data: &T, _env: &druid::Env) {} 12 | fn layout( 13 | &mut self, 14 | _ctx: &mut druid::LayoutCtx, 15 | _bc: &druid::BoxConstraints, 16 | _data: &T, 17 | _env: &druid::Env, 18 | ) -> druid::Size { 19 | self.size 20 | } 21 | fn paint(&mut self, _ctx: &mut druid::PaintCtx, _data: &T, _env: &druid::Env) {} 22 | } 23 | -------------------------------------------------------------------------------- /src/widgets/extension.rs: -------------------------------------------------------------------------------- 1 | use druid::{ 2 | widget::{Controller, ControllerHost}, 3 | Data, Env, Event, EventCtx, KbKey, KeyEvent, Widget, Selector, 4 | }; 5 | 6 | pub struct OnEnter { 7 | action: Box, 8 | } 9 | impl OnEnter { 10 | pub fn new(action: impl Fn(&mut EventCtx, &mut T, &Env) + 'static) -> Self { 11 | OnEnter { 12 | action: Box::new(action), 13 | } 14 | } 15 | } 16 | 17 | impl> Controller for OnEnter { 18 | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 19 | if let Event::KeyDown(KeyEvent { key: KbKey::Enter, .. }) = event { 20 | (self.action)(ctx, data, env); 21 | ctx.set_handled(); 22 | } else { 23 | child.event(ctx, event, data, env) 24 | } 25 | } 26 | } 27 | 28 | pub struct SendData { 29 | action: Box, 30 | } 31 | impl SendData { 32 | pub fn new(action: impl Fn(&mut EventCtx, &mut T, &String, &Env) + 'static) -> Self { 33 | SendData { 34 | action: Box::new(action), 35 | } 36 | } 37 | } 38 | 39 | impl> Controller for SendData { 40 | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, state: &mut T, env: &Env) { 41 | match event { 42 | Event::Command(cmd) if cmd.is(super::bottom_panel::SEND_STRING_DATA) => { 43 | let data = cmd.get_unchecked(super::bottom_panel::SEND_STRING_DATA); 44 | (self.action)(ctx, state, data, env); 45 | } 46 | _ => (), 47 | } 48 | child.event(ctx, event, state, env) 49 | } 50 | } 51 | 52 | const AUTO_FOCUS: Selector<()> = Selector::new("nonepad.extension.autofocus"); 53 | 54 | pub struct TakeFocus; 55 | impl TakeFocus { 56 | pub fn new() -> Self { 57 | TakeFocus {} 58 | } 59 | } 60 | impl> Controller for TakeFocus { 61 | fn event(&mut self, child: &mut W, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { 62 | match event { 63 | Event::Command(cmd) if cmd.is(AUTO_FOCUS) => { 64 | ctx.request_focus(); 65 | ctx.set_handled(); 66 | } 67 | _ => (), 68 | } 69 | child.event(ctx, event, data, env) 70 | } 71 | fn lifecycle( 72 | &mut self, 73 | child: &mut W, 74 | ctx: &mut druid::LifeCycleCtx, 75 | event: &druid::LifeCycle, 76 | data: &T, 77 | env: &Env, 78 | ) { 79 | if let druid::LifeCycle::WidgetAdded = event { 80 | ctx.submit_command(AUTO_FOCUS.to(ctx.widget_id())); 81 | } 82 | child.lifecycle(ctx, event, data, env) 83 | } 84 | } 85 | 86 | pub trait Extension: Widget + Sized + 'static { 87 | fn on_enter(self, f: impl Fn(&mut EventCtx, &mut T, &Env) + 'static) -> ControllerHost> { 88 | ControllerHost::new(self, OnEnter::new(f)) 89 | } 90 | fn focus(self) -> ControllerHost { 91 | ControllerHost::new(self, TakeFocus::new()) 92 | } 93 | fn on_data_received( 94 | self, 95 | f: impl Fn(&mut EventCtx, &mut T, &String, &Env) + 'static, 96 | ) -> ControllerHost> { 97 | ControllerHost::new(self, SendData::new(f)) 98 | } 99 | } 100 | 101 | impl + 'static> Extension for W {} 102 | -------------------------------------------------------------------------------- /src/widgets/mod.rs: -------------------------------------------------------------------------------- 1 | mod empty; 2 | mod extension; 3 | mod palette_view; 4 | pub mod editor_view; 5 | pub mod bottom_panel; 6 | pub mod window; 7 | pub mod text_buffer; 8 | 9 | pub use empty::*; 10 | pub use extension::*; 11 | pub use palette_view::*; 12 | 13 | -------------------------------------------------------------------------------- /src/widgets/palette_view.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::Ordering::Equal; 2 | use std::rc::Rc; 3 | use std::sync::Arc; 4 | 5 | use druid::im::Vector; 6 | use druid::piet::{Text, TextLayout, TextLayoutBuilder}; 7 | use druid::widget::{Flex, Label, Padding, TextBox}; 8 | use druid::{ 9 | Affine, Color, Data, Env, Event, EventCtx, KbKey, KeyEvent, Lens, LifeCycle, Point, Rect, RenderContext, 10 | Selector, Size, Widget, WidgetExt, WidgetId, WidgetPod, 11 | }; 12 | 13 | use sublime_fuzzy::best_match; 14 | 15 | use crate::theme::THEME; 16 | 17 | use super::editor_view::EditorView; 18 | use super::text_buffer::EditStack; 19 | use super::window::{NPWindow, NPWindowState}; 20 | 21 | const FILTER: Selector<()> = Selector::new("nonepad.editor.palette.filter"); 22 | 23 | #[derive(Debug, Data, Clone, Default)] 24 | pub struct Item { 25 | title: Arc, 26 | description: Arc, 27 | filtered: bool, 28 | score: isize, 29 | } 30 | 31 | impl Item { 32 | pub fn new(title: &str, description: &str) -> Self { 33 | Self { 34 | title: Arc::new(title.into()), 35 | description: Arc::new(description.into()), 36 | filtered: false, 37 | score: 0, 38 | } 39 | } 40 | } 41 | 42 | macro_rules! item { 43 | ($($n : expr), + $(,) ?) => {{ 44 | let mut v = Vector::new(); 45 | $(v.push_back(Item::new($n,"") );)+ 46 | v 47 | }}; 48 | } 49 | pub(crate) use item; 50 | 51 | #[derive(Debug, Data, Lens, Clone, Default)] 52 | pub struct PaletteViewState { 53 | title: String, 54 | filter: String, 55 | selected_idx: usize, 56 | list: Option>, 57 | visible_list: Option>, 58 | bbox: Rect, 59 | } 60 | 61 | impl PaletteViewState { 62 | fn apply_filter(&mut self) { 63 | if let Some(l) = &mut self.list { 64 | if self.filter.len() == 0 { 65 | for s in l.iter_mut() { 66 | s.filtered = false; 67 | s.score = 0; 68 | } 69 | self.visible_list = Some(l.iter().enumerate().map(|i| (i.0, i.1.clone())).collect()); 70 | } else { 71 | for s in l.iter_mut() { 72 | if let Some(m) = best_match(&self.filter, &s.title) { 73 | s.filtered = false; 74 | s.score = m.score(); 75 | } else { 76 | s.filtered = true; 77 | } 78 | } 79 | let mut vl: Vector<(usize, Item)> = l 80 | .iter() 81 | .enumerate() 82 | .filter(|c| !c.1.filtered) 83 | .map(|i| (i.0, i.1.clone())) 84 | .collect(); 85 | vl.sort_by(|l, r| { 86 | let result = r.1.score.cmp(&l.1.score); 87 | if result == Equal { 88 | l.1.title.cmp(&r.1.title) 89 | } else { 90 | result 91 | } 92 | }); 93 | self.visible_list = Some(vl); 94 | } 95 | } 96 | } 97 | 98 | fn prev(&mut self) { 99 | if self.selected_idx > 0 { 100 | self.selected_idx -= 1; 101 | } 102 | } 103 | fn next(&mut self) { 104 | if let Some(l) = &self.visible_list { 105 | if self.selected_idx < l.len() - 1 { 106 | self.selected_idx += 1; 107 | } 108 | } 109 | } 110 | } 111 | 112 | pub struct PaletteView { 113 | inner: WidgetPod>, 114 | textbox_id: WidgetId, 115 | action: Option, 116 | } 117 | 118 | impl PaletteView { 119 | pub(super) fn new() -> Self { 120 | let textbox_id = WidgetId::next(); 121 | PaletteView { 122 | inner: WidgetPod::new(build(textbox_id)), 123 | textbox_id, 124 | action: None, 125 | } 126 | } 127 | pub(super) fn init( 128 | &mut self, 129 | data: &mut PaletteViewState, 130 | title: String, 131 | list: Option>, 132 | action: Option, 133 | ) { 134 | data.list = list.clone(); 135 | data.title = title.to_owned(); 136 | data.selected_idx = 0; 137 | data.filter.clear(); 138 | self.action = action; 139 | data.visible_list = list.map(|l| l.iter().enumerate().map(|i| (i.0, i.1.clone())).collect()) 140 | } 141 | pub fn take_focus(&self, ctx: &mut EventCtx) { 142 | ctx.set_focus(self.textbox_id); 143 | } 144 | } 145 | 146 | impl Widget for PaletteView { 147 | fn event( 148 | &mut self, 149 | ctx: &mut druid::EventCtx, 150 | event: &druid::Event, 151 | data: &mut PaletteViewState, 152 | env: &druid::Env, 153 | ) { 154 | match event { 155 | Event::KeyDown(k) => match k { 156 | KeyEvent { 157 | key: druid::keyboard_types::Key::ArrowUp, 158 | .. 159 | } => { 160 | data.prev(); 161 | ctx.set_handled(); 162 | } 163 | KeyEvent { 164 | key: druid::keyboard_types::Key::ArrowDown, 165 | .. 166 | } => { 167 | data.next(); 168 | ctx.set_handled(); 169 | } 170 | KeyEvent { key: KbKey::Enter, .. } => { 171 | ctx.submit_command(CLOSE_PALETTE); 172 | 173 | if let Some(f) = self.action.take() { 174 | match &data.visible_list { 175 | Some(l) => { 176 | if let Some(item) = l.get(data.selected_idx) { 177 | ctx.submit_command( 178 | PALETTE_CALLBACK.with( 179 | ( 180 | PaletteResult { 181 | index: item.0, 182 | name: item.1.title.clone(), 183 | }, 184 | f, 185 | ))); 186 | } 187 | } 188 | None => { 189 | ctx.submit_command( 190 | PALETTE_CALLBACK.with( 191 | ( 192 | PaletteResult { 193 | index: 0, 194 | name: Arc::new(data.filter.clone()), 195 | }, 196 | f, 197 | ))); 198 | } 199 | } 200 | } 201 | 202 | ctx.set_handled(); 203 | } 204 | KeyEvent { key: KbKey::Escape, .. } => { 205 | ctx.submit_command(CLOSE_PALETTE); 206 | ctx.set_handled(); 207 | } 208 | _ => { 209 | self.inner.event(ctx, event, data, env); 210 | } 211 | }, 212 | Event::Command(c) if c.is(FILTER) => { 213 | data.apply_filter(); 214 | data.selected_idx = 0; 215 | ctx.request_paint(); 216 | ctx.set_handled(); 217 | } 218 | _ => { 219 | self.inner.event(ctx, event, data, env); 220 | } 221 | } 222 | } 223 | 224 | fn lifecycle( 225 | &mut self, 226 | ctx: &mut druid::LifeCycleCtx, 227 | event: &druid::LifeCycle, 228 | data: &PaletteViewState, 229 | env: &druid::Env, 230 | ) { 231 | self.inner.lifecycle(ctx, event, data, env); 232 | } 233 | 234 | fn update( 235 | &mut self, 236 | ctx: &mut druid::UpdateCtx, 237 | old_data: &PaletteViewState, 238 | data: &PaletteViewState, 239 | env: &druid::Env, 240 | ) { 241 | self.inner.update(ctx, data, env); 242 | 243 | if old_data.selected_idx != data.selected_idx || !old_data.filter.same(&data.filter) { 244 | ctx.request_paint(); 245 | } 246 | if !old_data.filter.same(&data.filter) { 247 | ctx.submit_command(FILTER.to(ctx.widget_id())) 248 | } 249 | } 250 | 251 | fn layout( 252 | &mut self, 253 | ctx: &mut druid::LayoutCtx, 254 | bc: &druid::BoxConstraints, 255 | data: &PaletteViewState, 256 | env: &druid::Env, 257 | ) -> druid::Size { 258 | self.inner.layout(ctx, bc, data, env) 259 | } 260 | 261 | fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &PaletteViewState, env: &druid::Env) { 262 | self.inner.paint(ctx, data, env); 263 | } 264 | } 265 | 266 | #[derive(Default)] 267 | struct PaletteList { 268 | total_height: f64, 269 | position: Point, 270 | } 271 | 272 | impl Widget for PaletteList { 273 | fn event(&mut self, _ctx: &mut druid::EventCtx, _event: &Event, _data: &mut PaletteViewState, _env: &druid::Env) {} 274 | 275 | fn lifecycle( 276 | &mut self, 277 | _ctx: &mut druid::LifeCycleCtx, 278 | _event: &druid::LifeCycle, 279 | _data: &PaletteViewState, 280 | _env: &druid::Env, 281 | ) { 282 | } 283 | 284 | fn update( 285 | &mut self, 286 | ctx: &mut druid::UpdateCtx, 287 | old_data: &PaletteViewState, 288 | data: &PaletteViewState, 289 | _env: &druid::Env, 290 | ) { 291 | if !old_data.list.same(&data.list) { 292 | ctx.request_layout(); 293 | } 294 | } 295 | 296 | fn layout( 297 | &mut self, 298 | ctx: &mut druid::LayoutCtx, 299 | bc: &druid::BoxConstraints, 300 | data: &PaletteViewState, 301 | env: &druid::Env, 302 | ) -> Size { 303 | let mut dy = 2.5; 304 | if let Some(l) = &data.visible_list { 305 | for item in l.iter() { 306 | let layout = ctx 307 | .text() 308 | //.new_text_layout(format!("{} {}", item.1.title.clone(), item.1.score)) 309 | .new_text_layout(item.1.title.clone()) 310 | .font(env.get(druid::theme::UI_FONT).family, 14.0) 311 | .text_color(env.get(druid::theme::TEXT_COLOR)) 312 | .alignment(druid::TextAlignment::Start) 313 | .max_width(500.) 314 | .build() 315 | .unwrap(); 316 | dy += layout.size().height + 2.; 317 | } 318 | } 319 | self.total_height = dy; 320 | Size::new(bc.max().width, self.total_height.min(500.)) 321 | } 322 | 323 | fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &PaletteViewState, env: &druid::Env) { 324 | if data.visible_list.is_some() { 325 | let size = ctx.size(); 326 | ctx.clip(Rect::ZERO.with_size(size)); 327 | let mut dy = 2.5; 328 | 329 | let mut layouts = Vec::new(); 330 | let mut selection_rect = Rect::ZERO; 331 | 332 | for (i, item) in data.visible_list.clone().unwrap().iter().enumerate() { 333 | let layout = ctx 334 | .text() 335 | //.new_text_layout(format!("{} {}", item.1.title.clone(), item.1.score)) 336 | .new_text_layout(item.1.title.clone()) 337 | .font(env.get(druid::theme::UI_FONT).family, 14.0) 338 | .text_color(env.get(druid::theme::TEXT_COLOR)) 339 | .alignment(druid::TextAlignment::Start) 340 | .max_width(500.) 341 | .build() 342 | .unwrap(); 343 | let height = layout.size().height; 344 | layouts.push((dy, layout)); 345 | if i == data.selected_idx { 346 | selection_rect = Rect::new(2.5, dy, size.width - 4.5, dy + height + 4.5); 347 | } 348 | 349 | dy += height + 2.; 350 | } 351 | 352 | if selection_rect.y0 < self.position.y { 353 | self.position.y = selection_rect.y0 354 | } 355 | if selection_rect.y1 > self.position.y + size.height { 356 | self.position.y = selection_rect.y1 - size.height 357 | } 358 | 359 | ctx.with_save(|ctx| { 360 | ctx.transform(Affine::translate((-self.position.x, -self.position.y))); 361 | 362 | ctx.fill( 363 | selection_rect, 364 | &env.get(crate::theme::SIDE_BAR_SECTION_HEADER_BACKGROUND), 365 | ); 366 | for l in layouts { 367 | ctx.draw_text(&l.1, (25.5, l.0)); 368 | } 369 | }); 370 | } 371 | } 372 | } 373 | 374 | // const SCROLLABLE_MOVE_VIEWPORT: Selector = Selector::new("nonepad.scrollable.move_viewport"); 375 | 376 | // pub trait Scrollable { 377 | // fn update_viewport(&mut self, rect: Rect); 378 | 379 | // } 380 | 381 | // pub trait ViewportMover { 382 | // fn move_viewport(&mut self, point: Point); 383 | // } 384 | 385 | // impl ViewportMover for EventCtx<'_, '_> { 386 | // fn move_viewport(&mut self, point: Point) { 387 | // self.submit_notification(SCROLLABLE_MOVE_VIEWPORT.with(point)); 388 | // } 389 | // } 390 | 391 | // struct Scroller { 392 | // inner: WidgetPod 393 | // } 394 | // impl Widget for Scroller { 395 | // fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &druid::Env) { 396 | // match event { 397 | // Event::Notification(notif) => if notif.is(SCROLLABLE_MOVE_VIEWPORT) { 398 | // self.inner.widget_mut().update_viewport(Rect::ZERO); 399 | // todo!() 400 | // } 401 | // _ => (), 402 | // } 403 | 404 | // } 405 | 406 | // fn lifecycle(&mut self, ctx: &mut druid::LifeCycleCtx, event: &LifeCycle, data: &T, env: &druid::Env) { 407 | 408 | // } 409 | 410 | // fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &T, data: &T, env: &druid::Env) { 411 | 412 | // } 413 | 414 | // fn layout(&mut self, ctx: &mut druid::LayoutCtx, bc: &druid::BoxConstraints, data: &T, env: &druid::Env) -> Size { 415 | // let r= self.inner.widget_mut().layout(ctx,bc,data,env); 416 | // self.inner.widget_mut().update_viewport(Rect::ZERO); 417 | // bc.max() 418 | // } 419 | 420 | // fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &T, env: &druid::Env) { 421 | 422 | // } 423 | // } 424 | 425 | struct EmptyWidget; 426 | impl Widget for EmptyWidget { 427 | fn event(&mut self, ctx: &mut EventCtx, event: &Event, _data: &mut T, _env: &druid::Env) { 428 | match event { 429 | Event::MouseDown(_) => { 430 | ctx.submit_command(CLOSE_PALETTE); 431 | } 432 | _ => (), 433 | } 434 | } 435 | 436 | fn lifecycle(&mut self, _ctx: &mut druid::LifeCycleCtx, _event: &LifeCycle, _data: &T, _env: &druid::Env) {} 437 | 438 | fn update(&mut self, _ctx: &mut druid::UpdateCtx, _old_data: &T, _data: &T, _env: &druid::Env) {} 439 | 440 | fn layout( 441 | &mut self, 442 | _ctx: &mut druid::LayoutCtx, 443 | bc: &druid::BoxConstraints, 444 | _data: &T, 445 | _env: &druid::Env, 446 | ) -> Size { 447 | bc.max() 448 | } 449 | 450 | fn paint(&mut self, _ctx: &mut druid::PaintCtx, _data: &T, _env: &druid::Env) {} 451 | } 452 | 453 | fn build(id: WidgetId) -> Flex { 454 | Flex::row() 455 | .with_flex_child(EmptyWidget, 0.5) 456 | .with_child( 457 | Flex::column() 458 | .with_child( 459 | Padding::new( 460 | 2., 461 | Flex::column() 462 | .with_child( 463 | Label::new(|data: &PaletteViewState, _env: &Env| format!("{}", data.title)) 464 | .with_text_size(12.0), 465 | ) 466 | .with_child( 467 | TextBox::new() 468 | .with_text_size(12.0) 469 | .fix_width(550.) 470 | .with_id(id) 471 | .lens(PaletteViewState::filter), 472 | ) 473 | .with_child(PaletteList::default()) 474 | .cross_axis_alignment(druid::widget::CrossAxisAlignment::Start), 475 | ) 476 | .background(Color::from_hex_str(&THEME.vscode.colors.side_bar_background).unwrap()) 477 | .rounded(4.), 478 | ) 479 | .with_flex_child(EmptyWidget, 1.) 480 | .fix_width(550.), 481 | ) 482 | .with_flex_child(EmptyWidget, 0.5) 483 | } 484 | 485 | #[derive(Clone)] 486 | pub(super) enum PaletteCommandType { 487 | Editor(Rc), 488 | Window(Rc), 489 | DialogEditor(Rc), 490 | DialogWindow(Rc), 491 | } 492 | 493 | pub(super) const SHOW_PALETTE_FOR_EDITOR: Selector<( 494 | WidgetId, 495 | String, 496 | Option>, 497 | Option>, 498 | )> = Selector::new("nonepad.palette.show_for_editor"); 499 | pub(super) const SHOW_PALETTE_FOR_WINDOW: Selector<( 500 | WidgetId, 501 | String, 502 | Option>, 503 | Option>, 504 | )> = Selector::new("nonepad.palette.show_for_window"); 505 | pub(super) const SHOW_DIALOG_FOR_EDITOR: Selector<( 506 | WidgetId, 507 | String, 508 | Option>, 509 | Option>, 510 | )> = Selector::new("nonepad.dialog.show_for_editor"); 511 | pub(super) const SHOW_DIALOG_FOR_WINDOW: Selector<( 512 | WidgetId, 513 | String, 514 | Option>, 515 | Option>, 516 | )> = Selector::new("nonepad.dialog.show_for_window"); 517 | 518 | pub(super) const PALETTE_CALLBACK: Selector<(PaletteResult, PaletteCommandType)> = 519 | Selector::new("nonepad.editor.execute_command"); 520 | pub(super) const CLOSE_PALETTE: Selector<()> = Selector::new("nonepad.palette.close"); 521 | 522 | trait ShowPalette { 523 | fn show_palette( 524 | &mut self, 525 | title: String, 526 | items: Option>, 527 | callback: Option>, 528 | ); 529 | } 530 | 531 | impl<'a, 'b, 'c> ShowPalette for EventCtx<'b, 'c> { 532 | fn show_palette( 533 | &mut self, 534 | title: String, 535 | items: Option>, 536 | callback: Option>, 537 | ) { 538 | self.submit_command(SHOW_PALETTE_FOR_EDITOR.with((self.widget_id(), title, items, callback))); 539 | } 540 | } 541 | 542 | impl<'a, 'b, 'c> ShowPalette for EventCtx<'b, 'c> { 543 | fn show_palette( 544 | &mut self, 545 | title: String, 546 | items: Option>, 547 | callback: Option>, 548 | ) { 549 | self.submit_command(SHOW_PALETTE_FOR_WINDOW.with((self.widget_id(), title, items, callback))); 550 | } 551 | } 552 | 553 | impl<'a, 'b, 'c> ShowPalette for EventCtx<'b, 'c> { 554 | fn show_palette( 555 | &mut self, 556 | title: String, 557 | items: Option>, 558 | callback: Option>, 559 | ) { 560 | self.submit_command(SHOW_DIALOG_FOR_EDITOR.with((self.widget_id(), title, items, callback))); 561 | } 562 | } 563 | 564 | impl<'a, 'b, 'c> ShowPalette for EventCtx<'b, 'c> { 565 | fn show_palette( 566 | &mut self, 567 | title: String, 568 | items: Option>, 569 | callback: Option>, 570 | ) { 571 | self.submit_command(SHOW_DIALOG_FOR_WINDOW.with((self.widget_id(), title, items, callback))); 572 | } 573 | } 574 | 575 | #[derive(Debug, PartialEq, Eq, Clone, Copy)] 576 | pub enum DialogResult { 577 | Ok, 578 | Cancel, 579 | } 580 | #[derive(Debug, Clone)] 581 | pub struct PaletteResult { 582 | pub index: usize, 583 | pub name: Arc, 584 | } 585 | 586 | pub struct Palette { 587 | title: Option, 588 | action: Option>, 589 | items: Option>, 590 | } 591 | 592 | impl Default for Palette { 593 | fn default() -> Self { 594 | Self { 595 | title: Default::default(), 596 | action: Default::default(), 597 | items: Default::default(), 598 | } 599 | } 600 | } 601 | 602 | impl Palette { 603 | fn new() -> Self { 604 | Palette::default() 605 | } 606 | pub fn title(mut self, title: &str) -> Self { 607 | self.title = Some(title.to_owned()); 608 | self 609 | } 610 | pub fn on_select(mut self, action: impl Fn(R, &mut EventCtx, &mut W, &mut D) + 'static) -> Self { 611 | self.action = Some(Rc::new(action)); 612 | self 613 | } 614 | pub fn items(mut self, items: Vector) -> Self { 615 | self.items = Some(items); 616 | self 617 | } 618 | } 619 | 620 | pub trait PaletteBuilder { 621 | fn palette(&self) -> Palette 622 | where 623 | Self: Sized, 624 | { 625 | Palette::::new() 626 | } 627 | fn dialog(&self) -> Palette 628 | where 629 | Self: Sized, 630 | { 631 | Palette::::new().items(item!["Ok", "Cancel"]) 632 | } 633 | fn alert(&self, title: &str) -> Palette 634 | where 635 | Self: Sized, 636 | { 637 | Palette::new().title(title).items(item!["Ok"]) 638 | } 639 | } 640 | 641 | impl PaletteBuilder for EditorView {} 642 | 643 | impl PaletteBuilder for NPWindow {} 644 | 645 | impl Palette { 646 | pub fn show(self, ctx: &mut EventCtx) { 647 | ctx.show_palette(self.title.unwrap_or_default(), self.items, self.action); 648 | } 649 | } 650 | 651 | impl Palette { 652 | pub fn show(self, ctx: &mut EventCtx) { 653 | ctx.show_palette(self.title.unwrap_or_default(), self.items, self.action); 654 | } 655 | } 656 | 657 | impl Palette { 658 | pub fn show(self, ctx: &mut EventCtx) { 659 | ctx.show_palette(self.title.unwrap_or_default(), self.items, self.action); 660 | } 661 | } 662 | 663 | impl Palette { 664 | pub fn show(self, ctx: &mut EventCtx) { 665 | ctx.show_palette(self.title.unwrap_or_default(), self.items, self.action); 666 | } 667 | } 668 | -------------------------------------------------------------------------------- /src/widgets/text_buffer/buffer.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | caret::{Caret, Carets}, 3 | file::{Indentation, LineFeed}, 4 | position::{Absolute, Column, Line, Point, Position, Relative}, 5 | rope_utils, SelectionLineRange, 6 | }; 7 | use druid::Data; 8 | use ropey::{Rope, RopeSlice}; 9 | use std::{ 10 | cell::Cell, 11 | ops::{Bound, Range, RangeBounds}, 12 | }; 13 | use uuid::Uuid; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct Buffer { 17 | pub rope: Rope, 18 | pub carets: Carets, 19 | pub(super) tabsize: usize, 20 | uuid: Uuid, 21 | max_visible_line_grapheme_len: Cell, 22 | } 23 | 24 | impl Data for Buffer { 25 | fn same(&self, other: &Self) -> bool { 26 | self.uuid == other.uuid && self.carets.same(&other.carets) 27 | } 28 | } 29 | 30 | impl Default for Buffer { 31 | fn default() -> Self { 32 | Self::new(super::file::Indentation::default().visible_len()) 33 | } 34 | } 35 | 36 | impl Buffer { 37 | pub fn new(tabsize: usize) -> Self { 38 | Self { 39 | rope: Rope::new(), 40 | carets: Carets::new(), 41 | uuid: Uuid::new_v4(), 42 | tabsize, 43 | max_visible_line_grapheme_len: Cell::new(0), 44 | } 45 | } 46 | 47 | pub fn same_content(&self, other: &Buffer) -> bool { 48 | self.uuid == other.uuid 49 | } 50 | 51 | pub fn from_rope(rope: Rope, tabsize: usize) -> Self { 52 | let b = Self { 53 | rope, 54 | carets: Carets::new(), 55 | uuid: Uuid::new_v4(), 56 | tabsize, 57 | max_visible_line_grapheme_len: Cell::new(0), 58 | }; 59 | for line in 0..100.min(b.len_lines()) { 60 | let l = b 61 | .line(line) 62 | .grapheme_len(&b) 63 | .index 64 | .max(b.max_visible_line_grapheme_len.get()); 65 | b.max_visible_line_grapheme_len.set(l); 66 | } 67 | b 68 | } 69 | 70 | /// Construct a string with tab replaced as space 71 | pub fn displayable_line( 72 | &self, 73 | line: Line, 74 | out: &mut String, 75 | rel_to_byte: &mut Vec, 76 | byte_to_rel: &mut Vec, 77 | ) { 78 | line.displayable_string(&self, self.tabsize, out, rel_to_byte, byte_to_rel); 79 | let l = line 80 | .grapheme_len(self) 81 | .index 82 | .max(self.max_visible_line_grapheme_len.get()); 83 | self.max_visible_line_grapheme_len.set(l); 84 | } 85 | 86 | pub fn max_visible_line_grapheme_len(&self) -> usize { 87 | self.max_visible_line_grapheme_len.get() 88 | } 89 | 90 | pub fn carets_on_line(&self, line: Line) -> impl Iterator { 91 | self.carets.iter().filter(move |c| c.line() == line) 92 | } 93 | 94 | pub fn selection_on_line(&self, line_idx: usize, ranges: &mut Vec) { 95 | ranges.clear(); 96 | for r in self.carets.iter().filter_map(move |c| { 97 | if !c.selection_is_empty() { 98 | let r = c.range(); 99 | match ( 100 | self.rope.byte_to_line(r.start.into()), 101 | self.rope.byte_to_line(r.end.into()), 102 | ) { 103 | (s, e) if s == e && s == line_idx => Some(SelectionLineRange::Range( 104 | self.position_to_point(r.start).relative.index..self.position_to_point(r.end).relative.index, 105 | )), 106 | (s, _) if s == line_idx => Some(SelectionLineRange::RangeFrom( 107 | self.position_to_point(r.start).relative.index.., 108 | )), 109 | (_, e) if e == line_idx => Some(SelectionLineRange::RangeTo( 110 | ..self.position_to_point(r.end).relative.index, 111 | )), 112 | (s, e) if line_idx < e && line_idx > s => Some(SelectionLineRange::RangeFull), 113 | _ => None, 114 | } 115 | } else { 116 | None 117 | } 118 | }) { 119 | ranges.push(r); 120 | } 121 | } 122 | 123 | pub fn position_to_absolute

(&self, pos: P) -> Absolute 124 | where 125 | P: Position, 126 | { 127 | pos.absolute(&self) 128 | } 129 | pub fn position_to_point

(&self, pos: P) -> Point 130 | where 131 | P: Position, 132 | { 133 | pos.point(&self) 134 | } 135 | 136 | pub fn len(&self) -> Absolute { 137 | self.rope.len_bytes().into() 138 | } 139 | 140 | pub fn len_lines(&self) -> usize { 141 | self.rope.len_lines() 142 | } 143 | 144 | pub fn point(&self, col: C, line: L) -> Point 145 | where 146 | C: Into, 147 | L: Into, 148 | { 149 | Point::new(col.into(), line.into(), &self) 150 | } 151 | 152 | pub fn line(&self, line_index: usize) -> Line { 153 | Line::from(line_index) 154 | } 155 | 156 | pub(super) fn line_slice(&self, line: L) -> RopeSlice 157 | where 158 | L: Into, 159 | { 160 | self.rope.line(line.into().index) 161 | } 162 | 163 | pub(super) fn absolute_to_line(&self, a: Absolute) -> Line { 164 | self.rope.byte_to_line(a.index).into() 165 | } 166 | 167 | pub(super) fn line_to_absolute(&self, line: L) -> Absolute 168 | where 169 | L: Into, 170 | { 171 | self.rope.line_to_byte(line.into().index).into() 172 | } 173 | 174 | pub fn word_start(&self, p: P) -> Absolute { 175 | Absolute::from(rope_utils::word_start(&self.slice(..), p.absolute(&self))) 176 | } 177 | pub fn word_end(&self, p: P) -> Absolute { 178 | Absolute::from(rope_utils::word_end(&self.slice(..), p.absolute(&self))) 179 | } 180 | 181 | pub fn char

(&self, pos: P) -> char 182 | where 183 | P: Position, 184 | { 185 | let a = pos.absolute(&self); 186 | self.rope.char(self.rope.byte_to_char(a.index)) 187 | } 188 | 189 | pub fn backward(&mut self, expand_selection: bool, word_boundary: bool) { 190 | let b = self.clone(); 191 | for s in &mut self.carets.iter_mut() { 192 | // TODO: Found a way to not clone, even if it's cheap 193 | s.move_backward(expand_selection, word_boundary, &b); 194 | } 195 | 196 | self.carets.merge(); 197 | } 198 | 199 | pub fn forward(&mut self, expand_selection: bool, word_boundary: bool) { 200 | let b = self.clone(); 201 | for s in &mut self.carets.iter_mut() { 202 | s.move_forward(expand_selection, word_boundary, &b); 203 | } 204 | 205 | self.carets.merge(); 206 | } 207 | 208 | pub fn up(&mut self, expand_selection: bool) { 209 | let b = self.clone(); 210 | for s in &mut self.carets.iter_mut() { 211 | s.move_up(expand_selection, &b); 212 | } 213 | 214 | self.carets.merge(); 215 | } 216 | pub fn down(&mut self, expand_selection: bool) { 217 | let b = self.clone(); 218 | for s in &mut self.carets.iter_mut() { 219 | s.move_down(expand_selection, &b); 220 | } 221 | 222 | self.carets.merge(); 223 | } 224 | pub fn duplicate_down(&mut self) { 225 | self.carets.sort_unstable(); 226 | 227 | if let Some(c) = self.carets.last().and_then(|c| c.duplicate_down(&self)) { 228 | self.carets.push(c); 229 | } 230 | self.carets.merge(); 231 | } 232 | 233 | pub fn duplicate_up(&mut self) { 234 | self.carets.sort_unstable(); 235 | 236 | if let Some(c) = self.carets.first().and_then(|c| c.duplicate_up(&self)) { 237 | self.carets.push(c); 238 | } 239 | self.carets.merge(); 240 | } 241 | 242 | pub fn have_selection(&self) -> bool { 243 | self.carets.iter().any(|c| !c.selection_is_empty()) 244 | } 245 | 246 | pub fn end(&mut self, expand_selection: bool) { 247 | let b = self.clone(); 248 | for s in &mut self.carets.iter_mut() { 249 | s.move_end(expand_selection, &b); 250 | } 251 | 252 | self.carets.merge(); 253 | } 254 | 255 | pub fn home(&mut self, expand_selection: bool) { 256 | let b = self.clone(); 257 | for s in &mut self.carets.iter_mut() { 258 | s.move_home(expand_selection, &b); 259 | } 260 | self.carets.merge(); 261 | } 262 | 263 | pub fn insert(&mut self, text: &str, expand_selection: bool) { 264 | for i in 0..self.carets.len() { 265 | let r = self.carets[i].range(); 266 | self.edit(&r, text); 267 | let b = self.clone(); 268 | self.carets[i].set_index(r.start + text.len(), !expand_selection, true, &b); 269 | } 270 | self.carets.merge(); 271 | } 272 | 273 | pub fn backspace(&mut self) -> bool { 274 | let mut did_nothing = true; 275 | for i in 0..self.carets.len() { 276 | if !self.carets[i].selection_is_empty() { 277 | // delete all the selection 278 | let r = self.carets[i].range(); 279 | self.edit(&r, ""); 280 | let b = self.clone(); 281 | self.carets[i].set_index(r.start, true, true, &b); 282 | 283 | did_nothing = false; 284 | } else if self.carets[i].index > 0.into() { 285 | // delete the preceding grapheme 286 | let r = rope_utils::prev_grapheme_boundary(&self.rope.slice(..), self.carets[i].index).into() 287 | ..self.carets[i].index; 288 | self.edit(&r, ""); 289 | let b = self.clone(); 290 | self.carets[i].set_index(r.start, true, true, &b); 291 | 292 | did_nothing = false; 293 | } else { 294 | continue; 295 | } 296 | } 297 | if !did_nothing { 298 | self.carets.merge(); 299 | return true; 300 | } 301 | false 302 | } 303 | 304 | pub fn delete(&mut self) -> bool { 305 | let mut did_nothing = true; 306 | for i in 0..self.carets.len() { 307 | if !self.carets[i].selection_is_empty() { 308 | let r = self.carets[i].range(); 309 | self.edit(&r, ""); 310 | let b = self.clone(); 311 | self.carets[i].set_index(r.start, true, true, &b); 312 | 313 | did_nothing = false; 314 | } else if self.carets[i].index < self.rope.len_bytes().into() { 315 | let r = self.carets[i].index 316 | ..rope_utils::next_grapheme_boundary(&self.rope.slice(..), self.carets[i].index).into(); 317 | self.edit(&r, ""); 318 | let b = self.clone(); 319 | self.carets[i].set_index(r.start, true, true, &b); 320 | 321 | did_nothing = false; 322 | } else { 323 | continue; 324 | } 325 | } 326 | if !did_nothing { 327 | self.carets.merge(); 328 | return true; 329 | } 330 | false 331 | } 332 | 333 | pub fn tab(&mut self, indentation: Indentation) { 334 | for i in 0..self.carets.len() { 335 | match self.carets[i].selected_lines_range(&self) { 336 | Some(line_range) if line_range.start() != line_range.end() => { 337 | // TODO: Find a better way to iterate over line of a selection 338 | for line_idx in line_range.start().index..line_range.end().index + 1 { 339 | let line_start: Absolute = self.rope.line_to_byte(line_idx).into(); 340 | let r = line_start..line_start; 341 | let text = match indentation { 342 | Indentation::Space(n) => " ".repeat(n).to_owned(), 343 | Indentation::Tab(_) => "\t".to_owned(), 344 | }; 345 | self.edit(&r, &text); 346 | } 347 | } 348 | _ => { 349 | let r = self.carets[i].range(); 350 | let text = match indentation { 351 | Indentation::Space(n) => { 352 | let start: usize = self.carets[i].col().into(); 353 | let nb_space = n - start % n; 354 | " ".repeat(nb_space).to_owned() 355 | } 356 | Indentation::Tab(_) => "\t".to_owned(), 357 | }; 358 | self.edit(&r, &text); 359 | let b = self.clone(); 360 | self.carets[i].set_index(r.start + Relative::from(text.len()), true, true, &b); 361 | } 362 | } 363 | } 364 | self.carets.merge(); 365 | } 366 | 367 | pub fn indent(&mut self, indentation: Indentation) { 368 | if self.have_selection() { 369 | return; 370 | } 371 | for i in 0..self.carets.len() { 372 | match self.carets[i].index.line(&self).index { 373 | 0 => (), 374 | max if max == self.len_lines() => (), 375 | l => { 376 | let l = Line::from(l); 377 | let indent = l.prev().unwrap().indentation(&self); 378 | let text = match indentation { 379 | Indentation::Space(_) => { 380 | " ".repeat(indent.into()).to_owned() 381 | } 382 | Indentation::Tab(_) => "\t".repeat(indent.index / indentation.visible_len()).to_owned(), 383 | }; 384 | self.edit(&Range{start: l.start(&self),end: l.start(&self)}, &text ); 385 | let b = self.clone(); 386 | self.carets[i].set_index(l.start(&b) + Relative::from(text.len()), true, true, &b); 387 | } 388 | 389 | } 390 | } 391 | } 392 | 393 | pub fn edit(&mut self, range: &Range, text: &str) { 394 | let insert_index = self.rope.byte_to_char(range.start.into()); 395 | let end_index = self.rope.byte_to_char(range.end.into()); 396 | let cr = insert_index..end_index; 397 | self.rope.remove(cr); 398 | self.rope.insert(insert_index, text); 399 | 400 | for i in 0..self.carets.len() { 401 | let b = self.clone(); 402 | self.carets[i].update_after_delete(range.start, range.end - range.start, &b); // TODO verify this 403 | let b = self.clone(); 404 | self.carets[i].update_after_insert(range.start, text.len().into(), &b); 405 | } 406 | self.carets.merge(); 407 | self.uuid = Uuid::new_v4(); 408 | } 409 | 410 | pub fn has_many_carets(&self) -> bool { 411 | self.carets.len() > 1 412 | } 413 | 414 | pub fn cancel_mutli_carets(&mut self) { 415 | self.carets.retain(|c| !c.is_clone); 416 | self.carets.merge(); 417 | } 418 | 419 | pub(super) fn slice(&self, r: R) -> RopeSlice 420 | where 421 | R: RangeBounds, 422 | { 423 | let start = start_bound_to_num(r.start_bound()).unwrap_or_else(|| Absolute::from(0)); 424 | let end = end_bound_to_num(r.end_bound()).unwrap_or_else(|| self.len()); 425 | 426 | self.rope 427 | .slice(self.rope.byte_to_char(start.index)..self.rope.byte_to_char(end.index)) 428 | } 429 | 430 | fn search_next_in_range(&self, s: &str, r: Range) -> Option { 431 | let mut index = r.start; 432 | let slice = self.slice(r); 433 | let s = s.to_lowercase(); 434 | slice.lines().find_map(|l| match l.to_string().to_lowercase().find(&s) { 435 | Some(i) => Some(index + i), 436 | None => { 437 | index += l.len_bytes(); 438 | None 439 | } 440 | }) 441 | } 442 | 443 | pub fn search_next(&mut self, s: &str) { 444 | let start_index = self.main_caret().index; 445 | 446 | let i = self 447 | .search_next_in_range(s, start_index..self.len()) 448 | .or_else(|| self.search_next_in_range(s, 0.into()..start_index)); 449 | if let Some(i) = i { 450 | self.cancel_mutli_carets(); 451 | self.move_main_caret_to(i, false, false); 452 | self.move_main_caret_to(i + s.len(), true, false); 453 | } 454 | } 455 | 456 | pub fn duplicate_cursor_from_str(&mut self, s: &str) { 457 | let start_index = self.last_created_caret().end(); 458 | let i = self 459 | .search_next_in_range(s, start_index..self.len()) 460 | .or_else(|| self.search_next_in_range(s, 0.into()..start_index)); 461 | if let Some(i) = i { 462 | if !self.carets.iter().any(|c| c.start() == i) { 463 | self.carets.sort_unstable(); 464 | let c = self.last_created_caret().duplicate_to(i, i + s.len(), &self); 465 | self.carets.push(c); 466 | } 467 | } 468 | } 469 | 470 | pub fn main_caret(&self) -> &Caret { 471 | self.carets.iter().find(|c| !c.is_clone).expect("No main cursor found!") 472 | } 473 | 474 | pub fn first_caret(&self) -> &Caret { 475 | self.carets 476 | .iter() 477 | .min_by_key(|c| c.start()) 478 | .expect("No cursor found!") 479 | } 480 | 481 | pub fn last_created_caret(&self) -> &Caret { 482 | self.carets 483 | .iter() 484 | .max_by_key(|c| c.generation) 485 | .expect("No cursor found!") 486 | } 487 | 488 | pub fn main_caret_mut(&mut self) -> &mut Caret { 489 | self.carets 490 | .iter_mut() 491 | .find(|c| c.is_clone) 492 | .expect("No main cursor found!") 493 | } 494 | 495 | pub fn move_main_caret_to

(&mut self, pos: P, expand_selection: bool, word_boundary: bool) 496 | where 497 | P: Position, 498 | { 499 | let p = self.position_to_absolute(pos); 500 | self.cancel_mutli_carets(); 501 | let b = self.clone(); 502 | if word_boundary { 503 | let start = self.word_end(p); 504 | let end = self.word_start(p); 505 | if expand_selection { 506 | if self.carets[0].index == self.carets[0].start() { 507 | self.carets[0].set_index(start, !expand_selection, true, &b); 508 | } else { 509 | self.carets[0].set_index(end, !expand_selection, true, &b); 510 | } 511 | } else { 512 | self.carets[0].set_index(start, true, true, &b); 513 | self.carets[0].set_index(end, false, true, &b); 514 | } 515 | } else { 516 | self.carets[0].set_index(p, !expand_selection, true, &b); 517 | } 518 | } 519 | 520 | pub fn selected_text(&self, line_feed: LineFeed) -> String { 521 | let mut s = String::new(); 522 | let multi = self.carets.len() > 1; 523 | for c in self.carets.iter() { 524 | for chuck in self 525 | .rope 526 | .slice(self.rope.byte_to_char(c.start().index)..self.rope.byte_to_char(c.end().index)) 527 | .chunks() 528 | { 529 | s.push_str(chuck) 530 | } 531 | if multi { 532 | s.push_str(&line_feed.to_str()) 533 | } 534 | } 535 | s 536 | } 537 | 538 | pub fn main_cursor_selected_text(&self) -> String { 539 | let mut s = String::new(); 540 | let c = self.main_caret(); 541 | for chuck in self 542 | .rope 543 | .slice(self.rope.byte_to_char(c.start().index)..self.rope.byte_to_char(c.end().index)) 544 | .chunks() 545 | { 546 | s.push_str(chuck) 547 | } 548 | s 549 | } 550 | 551 | pub fn cancel_selection(&mut self) { 552 | for c in &mut self.carets.iter_mut() { 553 | c.cancel_selection(); 554 | } 555 | } 556 | 557 | pub fn select_all(&mut self) { 558 | self.cancel_mutli_carets(); 559 | self.cancel_selection(); 560 | self.move_main_caret_to(Absolute::from(0), false, false); 561 | self.move_main_caret_to(self.len(), true, false); 562 | } 563 | 564 | pub fn select_line(&mut self, line: Line, expand_selection: bool) { 565 | self.cancel_mutli_carets(); 566 | 567 | if !expand_selection { 568 | self.cancel_selection(); 569 | self.move_main_caret_to(line.start(&self), false, false); 570 | self.move_main_caret_to(line.end(&self), true, false); 571 | } else if self.main_caret().start() == self.main_caret().index { 572 | self.move_main_caret_to(line.start(&self), true, false); 573 | } else { 574 | self.move_main_caret_to(line.end(&self), true, false); 575 | } 576 | } 577 | 578 | pub fn caret_display_info(&self) -> String { 579 | if !self.has_many_carets() { 580 | format!( 581 | "Ln {}, Col {}", 582 | self.carets[0].line().index + 1, 583 | self.carets[0].col().index + 1 584 | ) 585 | } else { 586 | format!("{} selections", self.carets.len()) 587 | } 588 | } 589 | } 590 | 591 | impl ToString for Buffer { 592 | fn to_string(&self) -> String { 593 | self.rope.to_string() 594 | } 595 | } 596 | 597 | #[inline(always)] 598 | pub(super) fn start_bound_to_num(b: Bound<&Absolute>) -> Option { 599 | match b { 600 | Bound::Included(n) => Some(*n), 601 | Bound::Excluded(n) => Some(*n + 1), 602 | Bound::Unbounded => None, 603 | } 604 | } 605 | 606 | #[inline(always)] 607 | pub(super) fn end_bound_to_num(b: Bound<&Absolute>) -> Option { 608 | match b { 609 | Bound::Included(n) => Some(*n + 1), 610 | Bound::Excluded(n) => Some(*n), 611 | Bound::Unbounded => None, 612 | } 613 | } 614 | -------------------------------------------------------------------------------- /src/widgets/text_buffer/caret.rs: -------------------------------------------------------------------------------- 1 | use super::position::{Absolute, Column, Line, Point, Position, Relative}; 2 | use super::{buffer::Buffer, rope_utils::*}; 3 | use druid::Data; 4 | 5 | use std::ops::Deref; 6 | use std::ops::DerefMut; 7 | use std::ops::{Range, RangeInclusive}; 8 | 9 | #[derive(Debug)] 10 | pub struct Carets { 11 | intern: Vec, 12 | } 13 | 14 | impl Carets { 15 | pub fn new() -> Self { 16 | Self { 17 | intern: vec![Caret::new()], 18 | } 19 | } 20 | 21 | pub fn merge(&mut self) { 22 | if self.intern.len() > 1 { 23 | self.intern 24 | .sort_unstable_by(|a, b| a.range().start.cmp(&b.range().start)) 25 | } 26 | let mut redo = true; 27 | 'outer: while redo { 28 | for i in 0..self.intern.len() - 1 { 29 | if self.intern[i].collide_with(&self.intern[i + 1]) { 30 | self.intern[i] = Caret::merge(&self.intern[i], &self.intern[i + 1]); 31 | self.intern.remove(i + 1); 32 | redo = true; 33 | continue 'outer; 34 | } 35 | } 36 | redo = false; 37 | } 38 | } 39 | } 40 | 41 | impl Default for Carets { 42 | fn default() -> Self { 43 | Self::new() 44 | } 45 | } 46 | 47 | impl Data for Carets { 48 | fn same(&self, other: &Self) -> bool { 49 | if self.intern.len() != other.intern.len() { 50 | return false; 51 | } 52 | for i in 0..self.intern.len() { 53 | if !self.intern[i].same(&other.intern[i]) { 54 | return false; 55 | } 56 | } 57 | true 58 | } 59 | } 60 | 61 | impl Clone for Carets { 62 | fn clone(&self) -> Self { 63 | let mut intern: Vec = self.intern.iter().filter(|c| c.is_clone).cloned().collect(); 64 | let mut first = self.intern.iter().find(|c| !c.is_clone).unwrap().clone(); 65 | first.is_clone = false; 66 | intern.push(first); 67 | intern.sort_unstable(); 68 | Self { intern } 69 | } 70 | } 71 | 72 | impl Deref for Carets { 73 | type Target = Vec; 74 | fn deref(&self) -> &Self::Target { 75 | &self.intern 76 | } 77 | } 78 | impl DerefMut for Carets { 79 | fn deref_mut(&mut self) -> &mut Self::Target { 80 | &mut self.intern 81 | } 82 | } 83 | 84 | #[derive(Debug, PartialEq, Eq, Default)] 85 | pub struct Caret { 86 | pub index: Absolute, 87 | selection: Absolute, 88 | point: Point, 89 | sticky_col: Column, 90 | pub is_clone: bool, 91 | pub(super) generation: usize, 92 | } 93 | 94 | impl Data for Caret { 95 | fn same(&self, other: &Self) -> bool { 96 | self.index == other.index && 97 | self.selection == other.selection 98 | } 99 | } 100 | 101 | impl Clone for Caret { 102 | fn clone(&self) -> Self { 103 | Self { 104 | index: self.index, 105 | selection: self.selection, 106 | point: self.point, 107 | sticky_col: self.sticky_col, 108 | is_clone: true, 109 | generation: self.generation + 1, 110 | } 111 | } 112 | } 113 | 114 | impl PartialOrd for Caret { 115 | fn partial_cmp(&self, other: &Caret) -> Option { 116 | Some(self.index.cmp(&other.index)) 117 | } 118 | } 119 | 120 | impl Ord for Caret { 121 | fn cmp(&self, other: &Self) -> core::cmp::Ordering { 122 | self.index.cmp(&other.index) 123 | } 124 | } 125 | 126 | impl Caret { 127 | pub fn new() -> Self { 128 | Self { 129 | index: Default::default(), 130 | selection: Default::default(), 131 | point: Default::default(), 132 | sticky_col: Default::default(), 133 | is_clone: false, 134 | generation: 0, 135 | } 136 | } 137 | fn from_point(p: Point, buffer: &Buffer) -> Self { 138 | Self { 139 | index: p.absolute(buffer), 140 | selection: p.absolute(buffer), 141 | point: p, 142 | sticky_col: p.col, 143 | is_clone: false, 144 | generation: 0, 145 | } 146 | } 147 | 148 | pub fn merge(c1: &Caret, c2: &Caret) -> Self { 149 | if c1.index < c1.selection { 150 | let (cstart, cend) = if c1.start() < c2.start() { (c1, c2) } else { (c2, c1) }; 151 | Self { 152 | index: cstart.index, 153 | point: cstart.point, 154 | sticky_col: cstart.sticky_col, 155 | selection: cend.end(), 156 | is_clone: cstart.is_clone && cend.is_clone, 157 | generation: cstart.generation.max(cend.generation), 158 | } 159 | } else { 160 | let (cstart, cend) = if c1.end() < c2.end() { (c1, c2) } else { (c2, c1) }; 161 | Self { 162 | index: cend.end(), 163 | point: cend.point, 164 | sticky_col: cend.sticky_col, 165 | selection: cstart.start(), 166 | is_clone: cstart.is_clone && cend.is_clone, 167 | generation: cstart.generation.max(cend.generation), 168 | } 169 | } 170 | } 171 | 172 | pub fn cancel_selection(&mut self) { 173 | self.selection = self.index; 174 | } 175 | 176 | pub fn selection_is_empty(&self) -> bool { 177 | self.selection == self.index 178 | } 179 | 180 | pub fn collide_with(&self, c1: &Caret) -> bool { 181 | self.range().contains(&c1.range().start) || (self.selection_is_empty() && self.index == c1.index) 182 | } 183 | 184 | pub fn range(&self) -> Range { 185 | if self.selection < self.index { 186 | self.selection..self.index 187 | } else { 188 | self.index..self.selection 189 | } 190 | } 191 | 192 | pub fn start(&self) -> Absolute { 193 | self.range().start 194 | } 195 | 196 | pub fn end(&self) -> Absolute { 197 | self.range().end 198 | } 199 | 200 | pub fn start_line(&self, buffer: &Buffer) -> Line { 201 | self.start().line(buffer) 202 | } 203 | 204 | pub fn end_line(&self, buffer: &Buffer) -> Line { 205 | self.end().line(buffer) 206 | } 207 | 208 | pub fn selected_lines_range(&self, buffer: &Buffer) -> Option> { 209 | if self.selection_is_empty() { 210 | None 211 | } else { 212 | Some(self.start_line(buffer)..=self.end_line(buffer)) 213 | } 214 | } 215 | 216 | pub fn line(&self) -> Line { 217 | self.point.line 218 | } 219 | 220 | pub fn relative(&self) -> Relative { 221 | self.point.relative 222 | } 223 | 224 | pub fn col(&self) -> Column { 225 | self.point.col 226 | } 227 | 228 | pub fn set_index(&mut self, index: Absolute, reset_selection: bool, reset_sticky_col: bool, buffer: &Buffer) { 229 | self.index = index; 230 | if reset_selection { 231 | self.selection = index; 232 | } 233 | self.point = self.index.point(buffer); 234 | if reset_sticky_col { 235 | self.sticky_col = self.point.col; 236 | } 237 | } 238 | 239 | pub fn move_up(&mut self, expand_selection: bool, buffer: &Buffer) { 240 | let pos = Point::new(self.sticky_col, self.line(), buffer).up(buffer); 241 | self.set_index(pos.absolute(buffer), !expand_selection, false, buffer); 242 | } 243 | 244 | pub fn move_down(&mut self, expand_selection: bool, buffer: &Buffer) { 245 | let pos = Point::new(self.sticky_col, self.line(), buffer).down(buffer); 246 | self.set_index(pos.absolute(buffer), !expand_selection, false, buffer); 247 | } 248 | 249 | pub fn move_backward(&mut self, expand_selection: bool, word_boundary: bool, buffer: &Buffer) { 250 | let index = if word_boundary { 251 | prev_word_boundary(&buffer.slice(..), self.index) 252 | } else { 253 | prev_grapheme_boundary(&buffer.slice(..), self.index) 254 | }; 255 | self.set_index(Absolute::from(index), !expand_selection, true, buffer); 256 | } 257 | 258 | pub fn move_forward(&mut self, expand_selection: bool, word_boundary: bool, buffer: &Buffer) { 259 | let index = if word_boundary { 260 | next_word_boundary(&buffer.slice(..), self.index) 261 | } else { 262 | next_grapheme_boundary(&buffer.slice(..), self.index) 263 | }; 264 | self.set_index(Absolute::from(index), !expand_selection, true, buffer); 265 | } 266 | 267 | pub fn duplicate_down(&self, buffer: &Buffer) -> Option { 268 | if self.line().next(buffer).is_some() { 269 | let mut c = Caret::from_point(self.point.down(buffer), buffer); 270 | c.is_clone = true; 271 | c.generation = self.generation + 1; 272 | Some(c) 273 | } else { 274 | None 275 | } 276 | } 277 | 278 | pub fn duplicate_up(&self, buffer: &Buffer) -> Option { 279 | if self.line().prev().is_some() { 280 | let mut c = Caret::from_point(self.point.up(buffer), buffer); 281 | c.is_clone = true; 282 | c.generation = self.generation + 1; 283 | Some(c) 284 | } else { 285 | None 286 | } 287 | } 288 | 289 | pub fn duplicate_to(&self, start: Absolute, end: Absolute, buffer: &Buffer) -> Self { 290 | let mut c = Caret::new(); 291 | c.set_index(start, true, true, buffer); 292 | c.set_index(end, false, true, buffer); 293 | c.is_clone = true; 294 | c.generation = self.generation + 1; 295 | c 296 | } 297 | 298 | pub fn move_end(&mut self, expand_selection: bool, buffer: &Buffer) { 299 | let index = self.line().end(buffer); 300 | self.set_index(index, !expand_selection, true, buffer); 301 | } 302 | 303 | pub fn move_home(&mut self, expand_selection: bool, buffer: &Buffer) { 304 | let index = self.line().start(buffer); 305 | let index2 = self.line().absolute_indentation(buffer); 306 | let index = if self.index>index2 || self.index == index { 307 | index2 308 | } else { 309 | index 310 | }; 311 | self.set_index(index, !expand_selection, true, buffer); 312 | } 313 | 314 | pub fn update_after_insert(&mut self, index: Absolute, delta: Relative, buffer: &Buffer) { 315 | if self.index > index { 316 | let col = self.col(); 317 | self.set_index(self.index + delta, false, false, buffer); 318 | // Update virtal column position only if the real column position changed 319 | if col != self.col() { 320 | self.sticky_col = col; 321 | } 322 | } 323 | if self.selection > index { 324 | self.selection += delta; 325 | } 326 | } 327 | pub fn update_after_delete(&mut self, index: Absolute, delta: Relative, buffer: &Buffer) { 328 | if self.index > index { 329 | let col = self.col(); 330 | self.set_index(self.index - delta, false, false, buffer); 331 | // Update virtal column position only if the real column position changed 332 | if col != self.col() { 333 | self.sticky_col = col; 334 | } 335 | } 336 | 337 | if self.selection > index { 338 | self.selection -= delta; 339 | } 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/widgets/text_buffer/edit_stack.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::Result; 3 | use std::ops::{Deref, DerefMut, Range, RangeFrom, RangeTo}; 4 | use std::path::{Path, PathBuf}; 5 | 6 | use super::buffer::Buffer; 7 | use super::file::TextFileInfo; 8 | use druid::Data; 9 | use once_cell::sync::Lazy; 10 | 11 | #[derive(Debug, Clone, Default)] 12 | pub struct EditStack { 13 | pub buffer: Buffer, 14 | undo_stack: Vec, 15 | redo_stack: Vec, 16 | pub file: TextFileInfo, 17 | pub filename: Option, 18 | dirty: bool, 19 | } 20 | 21 | impl Data for EditStack { 22 | fn same(&self, other: &Self) -> bool { 23 | self.buffer.same(&other.buffer) 24 | && self.file == other.file 25 | && self.filename == other.filename 26 | && self.dirty == other.dirty 27 | } 28 | } 29 | 30 | static AUTO_INSERT_CHARMAP: Lazy> = Lazy::new(|| { 31 | let mut m = HashMap::new(); 32 | m.insert("{", "{}"); 33 | m.insert("(", "()"); 34 | m.insert("<", "<>"); 35 | m.insert("[", "[]"); 36 | m.insert("\"", "\"\""); 37 | m 38 | }); 39 | 40 | impl EditStack { 41 | pub fn new() -> Self { 42 | Default::default() 43 | } 44 | 45 | pub fn selected_text(&self) -> String { 46 | self.buffer.selected_text(self.file.linefeed) 47 | } 48 | 49 | pub fn main_cursor_selected_text(&self) -> String { 50 | self.buffer.main_cursor_selected_text() 51 | } 52 | 53 | pub fn from_file>(path: P) -> Result { 54 | let file = TextFileInfo::load(&path)?; 55 | let buffer = Buffer::from_rope(file.1, file.0.indentation.visible_len()); 56 | Ok(Self { 57 | buffer, 58 | undo_stack: Vec::new(), 59 | redo_stack: Vec::new(), 60 | file: file.0, 61 | filename: Some(path.as_ref().to_path_buf()), 62 | dirty: false, 63 | }) 64 | } 65 | 66 | pub fn open

(&mut self, path: P) -> Result<()> 67 | where 68 | P: AsRef, 69 | { 70 | let editor = EditStack::from_file(path)?; 71 | let _ = std::mem::replace(self, editor); 72 | Ok(()) 73 | } 74 | 75 | pub fn reload(&mut self) -> Result<()> { 76 | if let Some(f) = &self.filename.clone() { 77 | self.open(f) 78 | } else { 79 | // unreachable? 80 | Ok(()) 81 | } 82 | } 83 | 84 | pub fn is_dirty(&self) -> bool { 85 | self.dirty 86 | } 87 | 88 | pub fn reset_dirty(&mut self) { 89 | self.dirty = false; 90 | } 91 | 92 | pub fn set_dirty(&mut self) { 93 | self.dirty = true; 94 | } 95 | 96 | pub fn save>(&mut self, path: P) -> Result<()> { 97 | self.file.save_as(&self.buffer, &path)?; 98 | self.filename = Some(path.as_ref().to_path_buf()); 99 | self.dirty = false; 100 | self.undo_stack.clear(); 101 | self.redo_stack.clear(); 102 | Ok(()) 103 | } 104 | 105 | pub fn undo(&mut self) { 106 | if let Some(buffer) = self.undo_stack.pop() { 107 | let b = std::mem::take(&mut self.buffer); 108 | self.redo_stack.push(b); 109 | self.buffer = buffer; 110 | } 111 | if self.undo_stack.is_empty() { 112 | self.dirty = false; 113 | } 114 | } 115 | 116 | pub fn redo(&mut self) { 117 | if let Some(buffer) = self.redo_stack.pop() { 118 | let b = std::mem::take(&mut self.buffer); 119 | self.undo_stack.push(b); 120 | self.buffer = buffer; 121 | } 122 | } 123 | 124 | fn push_edit(&mut self, buffer: Buffer) { 125 | let b = std::mem::take(&mut self.buffer); 126 | self.undo_stack.push(b); 127 | self.buffer = buffer; 128 | self.redo_stack.clear(); 129 | self.dirty = true; 130 | } 131 | 132 | pub fn insert(&mut self, text: &str) { 133 | let mut buf = self.buffer.clone(); 134 | 135 | match text { 136 | linefeed if linefeed == self.file.linefeed.to_str() => { 137 | buf.insert(text, false); 138 | buf.indent(self.file.indentation); 139 | } 140 | s if AUTO_INSERT_CHARMAP.get(s).is_some() => { 141 | let inner_text = buf.selected_text(self.file.linefeed); 142 | buf.insert(AUTO_INSERT_CHARMAP[text], false); 143 | buf.backward(false, false); 144 | buf.insert(&inner_text, true); 145 | } 146 | _ => { 147 | buf.insert(text, false); 148 | } 149 | } 150 | 151 | self.push_edit(buf); 152 | } 153 | 154 | pub fn backspace(&mut self) { 155 | let mut buf = self.buffer.clone(); 156 | 157 | // TODO check if old buf is same that new with the Data trait 158 | if buf.backspace() { 159 | self.push_edit(buf); 160 | } 161 | } 162 | 163 | pub fn delete(&mut self) { 164 | let mut buf = self.buffer.clone(); 165 | 166 | if buf.delete() { 167 | self.push_edit(buf); 168 | } 169 | } 170 | 171 | pub fn tab(&mut self) { 172 | let mut buf = self.buffer.clone(); 173 | buf.tab(self.file.indentation); 174 | self.push_edit(buf); 175 | } 176 | } 177 | 178 | impl Deref for EditStack { 179 | type Target = Buffer; 180 | fn deref(&self) -> &Self::Target { 181 | &self.buffer 182 | } 183 | } 184 | 185 | impl DerefMut for EditStack { 186 | fn deref_mut(&mut self) -> &mut Self::Target { 187 | &mut self.buffer 188 | } 189 | } 190 | 191 | #[derive(Debug, Clone)] 192 | pub enum SelectionLineRange { 193 | Range(Range), 194 | RangeTo(RangeTo), 195 | RangeFrom(RangeFrom), 196 | RangeFull, 197 | } 198 | -------------------------------------------------------------------------------- /src/widgets/text_buffer/file.rs: -------------------------------------------------------------------------------- 1 | use chardetng::EncodingDetector; 2 | use encoding_rs::{Encoding, UTF_8}; 3 | use ropey::{Rope, RopeSlice}; 4 | use syntect::parsing::SyntaxReference; 5 | use std::borrow::Cow; 6 | use std::fs; 7 | use std::io::{Read, Result, Write}; 8 | use std::{fmt::Display, path::Path}; 9 | use super::syntax::SYNTAXSET; 10 | 11 | use super::buffer::Buffer; 12 | 13 | #[derive(Debug, Clone)] 14 | pub struct TextFileInfo { 15 | pub encoding: &'static Encoding, 16 | pub bom: Option>, 17 | pub linefeed: LineFeed, 18 | pub indentation: Indentation, 19 | pub syntax: &'static SyntaxReference 20 | } 21 | 22 | impl Default for TextFileInfo { 23 | fn default() -> Self { 24 | TextFileInfo { 25 | encoding: UTF_8, 26 | bom: None, 27 | linefeed: Default::default(), 28 | indentation: Default::default(), 29 | syntax: SYNTAXSET.find_syntax_plain_text(), 30 | } 31 | } 32 | } 33 | 34 | impl PartialEq for TextFileInfo { 35 | fn eq(&self, other: &Self) -> bool { 36 | self.encoding == other.encoding && self.bom == other.bom && self.linefeed == other.linefeed && self.indentation == other.indentation && self.syntax.name == other.syntax.name 37 | } 38 | } 39 | 40 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 41 | pub enum LineFeed { 42 | Cr, 43 | Lf, 44 | CrLf, 45 | } 46 | 47 | impl Display for LineFeed { 48 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 49 | match self { 50 | LineFeed::Cr => write!(f, "CR"), 51 | LineFeed::Lf => write!(f, "LF"), 52 | LineFeed::CrLf => write!(f, "CRLF"), 53 | } 54 | } 55 | } 56 | 57 | impl Default for LineFeed { 58 | fn default() -> Self { 59 | #[cfg(target_os = "windows")] 60 | return LineFeed::CrLf; 61 | #[cfg(not(target_os = "windows"))] 62 | return LineFeed::Lf; 63 | } 64 | } 65 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 66 | pub enum Indentation { 67 | Tab(usize), 68 | Space(usize), 69 | } 70 | 71 | impl Display for Indentation { 72 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 73 | match self { 74 | Indentation::Tab(x) => write!(f, "Tab ({})", x), 75 | Indentation::Space(x) => write!(f, "Space ({})", x), 76 | } 77 | } 78 | } 79 | 80 | impl Indentation { 81 | pub fn visible_len(&self) -> usize { 82 | match *self { 83 | Indentation::Tab(l) => l, 84 | Indentation::Space(l) => l, 85 | } 86 | } 87 | 88 | pub fn len_as_byte(&self) -> usize { 89 | match *self { 90 | Indentation::Tab(_) => 1, 91 | Indentation::Space(l) => l, 92 | } 93 | } 94 | } 95 | 96 | impl Default for Indentation { 97 | fn default() -> Self { 98 | Indentation::Space(4) 99 | } 100 | } 101 | 102 | impl LineFeed { 103 | pub fn to_str(self) -> &'static str { 104 | match self { 105 | LineFeed::Cr => "\r", 106 | LineFeed::Lf => "\n", 107 | LineFeed::CrLf => "\r\n", 108 | } 109 | } 110 | } 111 | 112 | impl TextFileInfo { 113 | pub fn load>(path: P) -> Result<(TextFileInfo, Rope)> { 114 | let syntax = if let Ok(s ) = SYNTAXSET.find_syntax_for_file(&path) { 115 | s.unwrap_or_else(|| SYNTAXSET.find_syntax_plain_text()) 116 | } else { 117 | SYNTAXSET.find_syntax_plain_text() 118 | }; 119 | 120 | //let syntax = SYNTAXSET.find_syntax_by_extension(&std::path::Path::extension(path.as_ref()).unwrap_or(&OsString::from("")).to_string_lossy()).unwrap_or_else(|| SYNTAXSET.find_syntax_plain_text()); 121 | let mut file = fs::File::open(&path)?; 122 | 123 | let mut detector = EncodingDetector::new(); 124 | let mut vec = Vec::new(); 125 | file.read_to_end(&mut vec)?; 126 | 127 | 128 | 129 | detector.feed(&vec, true); 130 | let encoding = Encoding::for_bom(&vec); 131 | 132 | 133 | 134 | match encoding { 135 | None => { 136 | let encoding = detector.guess(None, true); 137 | 138 | let buffer = Rope::from_str(&encoding.decode_with_bom_removal(&vec).0); 139 | let linefeed = detect_linefeed(&buffer.slice(..)); 140 | let indentation = detect_indentation(&buffer.slice(..)); 141 | 142 | //crate::syntax::stats(buffer.to_string(), syntax); 143 | 144 | Ok(( 145 | TextFileInfo { 146 | encoding, 147 | bom: None, 148 | linefeed, 149 | indentation, 150 | syntax 151 | }, 152 | buffer, 153 | )) 154 | } 155 | Some((encoding, bom_size)) => { 156 | let bom = { 157 | let mut v = Vec::new(); 158 | v.extend_from_slice(&vec[0..bom_size]); 159 | v 160 | }; 161 | let buffer = Rope::from_str(&encoding.decode_with_bom_removal(&vec).0); 162 | let linefeed = detect_linefeed(&buffer.slice(..)); 163 | let indentation = detect_indentation(&buffer.slice(..)); 164 | 165 | //crate::syntax::stats(buffer.to_string(), syntax); 166 | 167 | Ok(( 168 | TextFileInfo { 169 | encoding, 170 | bom: Some(bom), 171 | linefeed, 172 | indentation, 173 | syntax 174 | }, 175 | buffer, 176 | )) 177 | } 178 | } 179 | } 180 | 181 | pub fn save_as>(&mut self, buffer: &Buffer, path: P) -> Result<()> { 182 | let mut file = fs::File::create(path.as_ref())?; 183 | let input = buffer.to_string(); 184 | let encoded_output = match self.encoding.name() { 185 | "UTF-16LE" => { 186 | let mut v = Vec::new(); 187 | input.encode_utf16().for_each(|i| v.extend_from_slice(&i.to_le_bytes())); 188 | Cow::from(v) 189 | } 190 | "UTF-16BE" => { 191 | let mut v = Vec::new(); 192 | input.encode_utf16().for_each(|i| v.extend_from_slice(&i.to_be_bytes())); 193 | Cow::from(v) 194 | } 195 | _ => self.encoding.encode(&input).0, 196 | }; 197 | 198 | if let Some(bom) = &self.bom { 199 | file.write_all(bom)?; 200 | } 201 | file.write_all(&encoded_output)?; 202 | Ok(()) 203 | } 204 | } 205 | 206 | /// Detect the carriage return type of the buffer 207 | fn detect_linefeed(input: &RopeSlice) -> LineFeed { 208 | let linefeed = Default::default(); 209 | 210 | if input.len_bytes() == 0 { 211 | return linefeed; 212 | } 213 | 214 | let mut cr = 0; 215 | let mut lf = 0; 216 | let mut crlf = 0; 217 | 218 | let mut chars = input.chars().take(1000); 219 | while let Some(c) = chars.next() { 220 | if c == '\r' { 221 | if let Some(c2) = chars.next() { 222 | if c2 == '\n' { 223 | crlf += 1; 224 | } else { 225 | cr += 1; 226 | } 227 | } 228 | } else if c == '\n' { 229 | lf += 1; 230 | } 231 | } 232 | 233 | if cr > crlf && cr > lf { 234 | LineFeed::Cr 235 | } else if lf > crlf && lf > cr { 236 | LineFeed::Lf 237 | } else { 238 | LineFeed::CrLf 239 | } 240 | } 241 | 242 | pub fn detect_indentation(input: &RopeSlice) -> Indentation { 243 | // detect Tabs first. If the first char of a line is more often a Tab 244 | // then we consider the indentation as tabulation. 245 | 246 | let mut tab = 0; 247 | let mut space = 0; 248 | for line in input.lines() { 249 | match line.chars().next() { 250 | Some(' ') => space += 1, 251 | Some('\t') => tab += 1, 252 | _ => (), 253 | } 254 | } 255 | if tab > space { 256 | // todo: get len from settings 257 | return Indentation::Tab(4); 258 | } 259 | 260 | // Algorythm from 261 | // https://medium.com/firefox-developer-tools/detecting-code-indentation-eff3ed0fb56b 262 | use std::collections::HashMap; 263 | let mut indents = HashMap::new(); 264 | let mut last = 0; 265 | 266 | for line in input.lines() { 267 | let width = line.chars().take_while(|c| *c == ' ').count(); 268 | let indent = if width < last { last - width } else { width - last }; 269 | if indent > 1 { 270 | (*indents.entry(indent).or_insert(0)) += 1; 271 | } 272 | last = width; 273 | } 274 | if let Some(i) = indents.iter().max_by(|x, y| x.1.cmp(y.1)) { 275 | Indentation::Space(*i.0) 276 | } else { 277 | Indentation::default() 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/widgets/text_buffer/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod buffer; 2 | mod caret; 3 | mod edit_stack; 4 | mod file; 5 | pub mod position; 6 | pub mod rope_utils; 7 | pub mod syntax; 8 | 9 | pub use edit_stack::*; 10 | pub use file::Indentation; 11 | 12 | -------------------------------------------------------------------------------- /src/widgets/text_buffer/position.rs: -------------------------------------------------------------------------------- 1 | use super::buffer::Buffer; 2 | use super::rope_utils::{next_graphem_len, prev_grapheme_boundary}; 3 | use druid::Data; 4 | use std::ops::Add; 5 | use std::ops::{AddAssign, Sub, SubAssign}; 6 | 7 | pub trait Position { 8 | fn absolute(&self, buffer: &Buffer) -> Absolute; 9 | fn point(&self, buffer: &Buffer) -> Point; 10 | fn line(&self, buffer: &Buffer) -> Line; 11 | fn up(&self, buffer: &Buffer) -> Self; 12 | fn down(&self, buffer: &Buffer) -> Self; 13 | //fn relative(&self, buffer: &Buffer) -> Relative; 14 | } 15 | 16 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Data)] 17 | pub struct Point { 18 | pub col: Column, 19 | pub line: Line, 20 | pub relative: Relative, 21 | } 22 | 23 | impl Position for Point { 24 | fn absolute(&self, buffer: &Buffer) -> Absolute { 25 | self.line.start(buffer) + self.relative 26 | } 27 | fn point(&self, _buffer: &Buffer) -> Point { 28 | *self 29 | } 30 | fn line(&self, _buffer: &Buffer) -> Line { 31 | self.line 32 | } 33 | 34 | fn up(&self, buffer: &Buffer) -> Self { 35 | let line = self.line(buffer).prev().unwrap_or(self.line); 36 | let col = if self.col > line.grapheme_len(buffer) { 37 | line.grapheme_len(buffer) 38 | } else { 39 | self.col 40 | }; 41 | Self::new(col, line, buffer) 42 | } 43 | fn down(&self, buffer: &Buffer) -> Self { 44 | let line = self.line(buffer).next(buffer).unwrap_or(self.line); 45 | let col = if self.col > line.grapheme_len(buffer) { 46 | line.grapheme_len(buffer) 47 | } else { 48 | self.col 49 | }; 50 | Self::new(col, line, buffer) 51 | } 52 | } 53 | 54 | impl Point { 55 | pub fn new(col: Column, line: Line, buffer: &Buffer) -> Self { 56 | let line = if line.index >= buffer.len_lines() { 57 | buffer.len_lines().into() 58 | } else { 59 | line 60 | }; 61 | Self { 62 | col, 63 | line, 64 | relative: super::rope_utils::column_to_relative(col, line, buffer), 65 | } 66 | } 67 | } 68 | 69 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Data)] 70 | pub struct Absolute { 71 | pub index: usize, 72 | } 73 | 74 | impl From for Absolute { 75 | fn from(index: usize) -> Self { 76 | Self { index } 77 | } 78 | } 79 | 80 | impl From for usize { 81 | fn from(src: Absolute) -> Self { 82 | src.index 83 | } 84 | } 85 | 86 | impl AddAssign for Absolute { 87 | fn add_assign(&mut self, rhs: Relative) { 88 | self.index += rhs.index; 89 | } 90 | } 91 | 92 | impl AddAssign for Absolute { 93 | fn add_assign(&mut self, rhs: usize) { 94 | self.index += rhs; 95 | } 96 | } 97 | 98 | impl SubAssign for Absolute { 99 | fn sub_assign(&mut self, rhs: Relative) { 100 | self.index -= rhs.index; 101 | } 102 | } 103 | 104 | impl Add for Absolute { 105 | type Output = Absolute; 106 | fn add(self, rhs: Relative) -> Self::Output { 107 | (rhs.index + self.index).into() 108 | } 109 | } 110 | 111 | impl Add for Absolute { 112 | type Output = Absolute; 113 | fn add(self, rhs: usize) -> Self::Output { 114 | (rhs + self.index).into() 115 | } 116 | } 117 | 118 | impl Sub for Absolute { 119 | type Output = Relative; 120 | fn sub(self, rhs: Absolute) -> Self::Output { 121 | Relative::from(self.index - rhs.index) 122 | } 123 | } 124 | 125 | impl Sub for Absolute { 126 | type Output = Absolute; 127 | fn sub(self, rhs: Relative) -> Self::Output { 128 | Absolute::from(self.index - rhs.index) 129 | } 130 | } 131 | 132 | impl Sub for Absolute { 133 | type Output = Absolute; 134 | fn sub(self, rhs: usize) -> Self::Output { 135 | Absolute::from(self.index - rhs) 136 | } 137 | } 138 | 139 | impl Position for Absolute { 140 | fn absolute(&self, _buffer: &Buffer) -> Absolute { 141 | *self 142 | } 143 | fn point(&self, buffer: &Buffer) -> Point { 144 | let line = self.line(buffer); // buffer.absolute_to_line(*self); 145 | let relative = *self - line.start(buffer); 146 | Point { 147 | line, 148 | relative, 149 | col: super::rope_utils::relative_to_column(relative, line, buffer), 150 | } 151 | } 152 | fn line(&self, buffer: &Buffer) -> Line { 153 | buffer.absolute_to_line(*self) 154 | } 155 | fn up(&self, buffer: &Buffer) -> Self { 156 | self.point(buffer).up(buffer).absolute(buffer) 157 | } 158 | fn down(&self, buffer: &Buffer) -> Self { 159 | self.point(buffer).down(buffer).absolute(buffer) 160 | } 161 | } 162 | 163 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Data)] 164 | pub struct Relative { 165 | pub index: usize, 166 | } 167 | 168 | impl From for Relative { 169 | fn from(index: usize) -> Self { 170 | Self { index } 171 | } 172 | } 173 | 174 | impl From for usize { 175 | fn from(src: Relative) -> Self { 176 | src.index 177 | } 178 | } 179 | 180 | impl AddAssign for Relative { 181 | fn add_assign(&mut self, rhs: usize) { 182 | self.index += rhs; 183 | } 184 | } 185 | 186 | impl PartialEq for Relative { 187 | fn eq(&self, other: &usize) -> bool { 188 | self.index == *other 189 | } 190 | } 191 | 192 | impl PartialOrd for Relative { 193 | fn partial_cmp(&self, other: &usize) -> Option { 194 | self.index.partial_cmp(other) 195 | } 196 | } 197 | 198 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Data)] 199 | pub struct Column { 200 | pub index: usize, 201 | } 202 | 203 | impl From for Column { 204 | fn from(index: usize) -> Self { 205 | Self { index } 206 | } 207 | } 208 | 209 | impl From for usize { 210 | fn from(src: Column) -> Self { 211 | src.index 212 | } 213 | } 214 | 215 | impl AddAssign for Column { 216 | fn add_assign(&mut self, rhs: usize) { 217 | self.index += rhs; 218 | } 219 | } 220 | 221 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Data)] 222 | pub struct Line { 223 | pub index: usize, 224 | } 225 | 226 | impl From for Line { 227 | fn from(index: usize) -> Self { 228 | Self { index } 229 | } 230 | } 231 | 232 | // impl Into for Line { 233 | // fn into(self) -> usize { 234 | // self.index 235 | // } 236 | // } 237 | 238 | impl Line { 239 | pub fn start(&self, buffer: &Buffer) -> Absolute { 240 | buffer.line_to_absolute(*self) 241 | } 242 | pub fn end(&self, buffer: &Buffer) -> Absolute { 243 | // TODO use self.next 244 | if self.index + 1 >= buffer.len_lines() { 245 | buffer.len() 246 | } else { 247 | Absolute::from(prev_grapheme_boundary( 248 | &buffer.slice(..), 249 | buffer.line_to_absolute(self.index + 1), 250 | )) 251 | } 252 | } 253 | 254 | pub fn byte_len(&self, buffer: &Buffer) -> Relative { 255 | self.end(buffer) - self.start(buffer) 256 | } 257 | 258 | pub fn grapheme_len(&self, buffer: &Buffer) -> Column { 259 | let mut col = Column::from(0); 260 | if self.index >= buffer.len_lines() { 261 | return col; 262 | } 263 | 264 | let slice = buffer.line_slice(*self); 265 | let mut it = slice.bytes().enumerate().peekable(); 266 | 'outer: loop { 267 | let l = match it.peek() { 268 | None => break 'outer, 269 | Some((_, b'\t')) => { 270 | let nb_space: usize = buffer.tabsize - col.index % buffer.tabsize; 271 | col += nb_space; 272 | 1 273 | } 274 | Some((i, _)) => { 275 | col += 1; 276 | next_graphem_len(&slice, *i) 277 | } 278 | }; 279 | it.nth(l-1); 280 | } 281 | 282 | col 283 | } 284 | pub fn prev(&self) -> Option { 285 | if self.index == 0 { 286 | None 287 | } else { 288 | Some(Self { index: self.index - 1 }) 289 | } 290 | } 291 | pub fn next(&self, buffer: &Buffer) -> Option { 292 | if self.index == buffer.len_lines() - 1 { 293 | None 294 | } else { 295 | Some(Self { index: self.index + 1 }) 296 | } 297 | } 298 | pub fn to_string(self, buffer: &Buffer) -> String { 299 | buffer.line_slice(self).to_string() 300 | } 301 | pub fn displayable_string( 302 | &self, 303 | buffer: &Buffer, 304 | tabsize: usize, 305 | out: &mut String, 306 | rel_to_byte: &mut Vec, 307 | byte_to_rel: &mut Vec, 308 | ) { 309 | out.clear(); 310 | rel_to_byte.clear(); 311 | byte_to_rel.clear(); 312 | if self.index >= buffer.len_lines() { 313 | return; 314 | } 315 | 316 | let mut rel_index = 0; 317 | let mut byte_index = 0; 318 | for c in buffer.line_slice(*self).chars() { 319 | match c { 320 | ' ' => { 321 | rel_to_byte.push(rel_index.into()); 322 | byte_to_rel.push(byte_index.into()); 323 | out.push(' '); 324 | rel_index += 1; 325 | byte_index += 1; 326 | } 327 | '\t' => { 328 | let nb_space = tabsize - rel_index % tabsize; 329 | rel_to_byte.push(rel_index.into()); 330 | for _ in 0..nb_space { 331 | byte_to_rel.push(byte_index.into()); 332 | } 333 | out.push_str(&" ".repeat(nb_space)); 334 | rel_index += nb_space; 335 | byte_index += 1; 336 | } 337 | _ => { 338 | out.push(c); 339 | for _ in rel_index..rel_index + c.len_utf8() { 340 | rel_to_byte.push(rel_index.into()); 341 | byte_to_rel.push(byte_index.into()); 342 | } 343 | rel_index += c.len_utf8(); 344 | byte_index += c.len_utf8(); 345 | } 346 | } 347 | } 348 | rel_to_byte.push(rel_index.into()); 349 | byte_to_rel.push(byte_index.into()); 350 | } 351 | 352 | pub fn absolute_indentation(&self, buffer: &Buffer) -> Absolute { 353 | let a = buffer.line_to_absolute(self.index); 354 | 355 | a + buffer 356 | .slice(a..) 357 | .bytes() 358 | .take_while(|c| matches!(c, b' ' | b'\t')) 359 | .count() 360 | } 361 | 362 | pub fn relative_indentation(&self, buffer: &Buffer) -> Relative { 363 | let a = buffer.line_to_absolute(self.index); 364 | buffer 365 | .slice(a..) 366 | .bytes() 367 | .take_while(|c| matches!(c, b' ' | b'\t')) 368 | .count() 369 | .into() 370 | } 371 | 372 | pub fn indentation(&self, buffer: &Buffer) -> Column { 373 | let mut col = Column::from(0); 374 | if self.index >= buffer.len_lines() { 375 | return col; 376 | } 377 | let slice = buffer.line_slice(*self); 378 | let mut it = slice.bytes().enumerate().peekable(); 379 | 'outer: loop { 380 | let l = match it.peek() { 381 | None => break 'outer, 382 | Some((_,b' ')) => { 383 | col+=1; 384 | 1 385 | } 386 | Some((_, b'\t')) => { 387 | let nb_space: usize = buffer.tabsize - col.index % buffer.tabsize; 388 | col += nb_space; 389 | 1 390 | } 391 | Some((_, _)) => { 392 | return col; 393 | } 394 | }; 395 | it.nth(l-1); 396 | } 397 | 398 | col 399 | } 400 | 401 | pub fn iter<'r>(&self, buffer: &'r Buffer) -> LineIterator<'r> { 402 | LineIterator { buffer, line: *self } 403 | } 404 | } 405 | 406 | pub struct LineIterator<'r> { 407 | buffer: &'r Buffer, 408 | line: Line, 409 | } 410 | 411 | impl<'r> Iterator for LineIterator<'r> { 412 | type Item = Line; 413 | fn next(&mut self) -> Option { 414 | self.line.next(self.buffer) 415 | } 416 | } 417 | 418 | impl SubAssign for Line { 419 | fn sub_assign(&mut self, rhs: usize) { 420 | if self.index > 0 { 421 | self.index -= rhs; 422 | } 423 | } 424 | } 425 | 426 | impl Sub for Line { 427 | type Output = Line; 428 | fn sub(self, rhs: Line) -> Self::Output { 429 | Line::from(self.index - rhs.index) 430 | } 431 | } 432 | 433 | impl Sub<&Line> for &Line { 434 | type Output = Line; 435 | fn sub(self, rhs: &Line) -> Self::Output { 436 | Line::from(self.index - rhs.index) 437 | } 438 | } 439 | 440 | #[cfg(test)] 441 | mod tests { 442 | use super::super::{buffer::Buffer, position::Column}; 443 | 444 | #[test] 445 | fn grapheme_len() { 446 | let mut input = Buffer::new(4); 447 | input.insert("Hello 😊︎ 😐︎ ☹︎ example", false); 448 | assert_eq!(input.line(0).grapheme_len(&input), Column::from(19)); 449 | } 450 | } 451 | -------------------------------------------------------------------------------- /src/widgets/text_buffer/rope_utils.rs: -------------------------------------------------------------------------------- 1 | use super::{ 2 | buffer::Buffer, 3 | position::{Column, Line, Relative}, 4 | }; 5 | use ropey::RopeSlice; 6 | use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete}; 7 | 8 | /// Finds the previous grapheme boundary before the given char position. 9 | pub fn prev_grapheme_boundary>(slice: &RopeSlice, byte_idx: U) -> usize { 10 | let byte_idx = byte_idx.into(); 11 | // Bounds check 12 | debug_assert!(byte_idx <= slice.len_bytes()); 13 | 14 | // Get the chunk with our byte index in it. 15 | let (mut chunk, mut chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx); 16 | 17 | // Set up the grapheme cursor. 18 | let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true); 19 | 20 | // Find the previous grapheme cluster boundary. 21 | loop { 22 | match gc.prev_boundary(chunk, chunk_byte_idx) { 23 | Ok(None) => return 0, 24 | Ok(Some(n)) => { 25 | let tmp = n - chunk_byte_idx; 26 | return chunk_byte_idx + tmp; 27 | } 28 | Err(GraphemeIncomplete::PrevChunk) => { 29 | let (a, b, _, _) = slice.chunk_at_byte(chunk_byte_idx - 1); 30 | chunk = a; 31 | chunk_byte_idx = b; 32 | } 33 | Err(GraphemeIncomplete::PreContext(n)) => { 34 | let ctx_chunk = slice.chunk_at_byte(n - 1).0; 35 | gc.provide_context(ctx_chunk, n - ctx_chunk.len()); 36 | } 37 | _ => unreachable!(), 38 | } 39 | } 40 | } 41 | 42 | /// Finds the next grapheme boundary after the given char position. 43 | pub fn next_grapheme_boundary>(slice: &RopeSlice, byte_idx: U) -> usize { 44 | let byte_idx = byte_idx.into(); 45 | // Bounds check 46 | debug_assert!(byte_idx <= slice.len_bytes()); 47 | 48 | // Get the chunk with our byte index in it. 49 | let (mut chunk, mut chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx); 50 | 51 | // Set up the grapheme cursor. 52 | let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true); 53 | 54 | // Find the next grapheme cluster boundary. 55 | loop { 56 | match gc.next_boundary(chunk, chunk_byte_idx) { 57 | Ok(None) => return slice.len_bytes(), 58 | Ok(Some(n)) => { 59 | let tmp = n - chunk_byte_idx; 60 | return chunk_byte_idx + tmp; 61 | } 62 | Err(GraphemeIncomplete::NextChunk) => { 63 | chunk_byte_idx += chunk.len(); 64 | let (a, b, _, _) = slice.chunk_at_byte(chunk_byte_idx); 65 | chunk = a; 66 | chunk_byte_idx = b; 67 | } 68 | Err(GraphemeIncomplete::PreContext(n)) => { 69 | let ctx_chunk = slice.chunk_at_byte(n - 1).0; 70 | gc.provide_context(ctx_chunk, n - ctx_chunk.len()); 71 | } 72 | _ => unreachable!(), 73 | } 74 | } 75 | } 76 | 77 | pub fn next_graphem_len>(slice: &RopeSlice, byte_idx: U) -> usize { 78 | let i = byte_idx.into(); 79 | next_grapheme_boundary(slice,i) - i 80 | } 81 | 82 | const WORD_BOUNDARY_PUCTUATION: [char; 31] = [ 83 | '`', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '=', '+', '[', '{', ']', '}', '\\', '|', ';', ':', 84 | '\'', '"', ',', '.', '<', '>', '/', '?', 85 | ]; 86 | const WORD_BOUNDARY_LINEFEED: [char; 2] = ['\n', '\r']; 87 | 88 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 89 | enum CharType { 90 | LineFeed, 91 | Space, 92 | Punctuation, 93 | Other, 94 | } 95 | 96 | fn char_type(c: char) -> CharType { 97 | if WORD_BOUNDARY_PUCTUATION.contains(&c) { 98 | CharType::Punctuation 99 | } else if WORD_BOUNDARY_LINEFEED.contains(&c) { 100 | CharType::LineFeed 101 | } else if c.is_whitespace() { 102 | CharType::Space 103 | } else { 104 | CharType::Other 105 | } 106 | } 107 | 108 | fn is_boundary(a: char, b: char) -> bool { 109 | char_type(a) != char_type(b) 110 | } 111 | 112 | pub fn next_word_boundary>(slice: &RopeSlice, byte_idx: U) -> usize { 113 | let mut i: usize = slice.byte_to_char(byte_idx.into()); 114 | 115 | // discard all space 116 | i += slice.chars_at(i).take_while(|c| c.is_whitespace()).count(); 117 | 118 | // if multi puctionation, skip to new non puctuation char 119 | let fp = slice 120 | .chars_at(i) 121 | .take_while(|c| WORD_BOUNDARY_PUCTUATION.contains(c)) 122 | .count(); 123 | i += fp; 124 | if i >= slice.len_chars() { 125 | return slice.len_bytes(); 126 | } 127 | let current_char = slice.char(i); 128 | if fp > 1 || (fp == 1 && char_type(current_char) != CharType::Other) { 129 | return slice.char_to_byte(i); 130 | } 131 | 132 | i += slice.chars_at(i).take_while(|c| !is_boundary(*c, current_char)).count(); 133 | 134 | slice.char_to_byte(i) 135 | } 136 | 137 | pub fn prev_word_boundary>(slice: &RopeSlice, byte_idx: U) -> usize { 138 | let mut i: usize = slice.byte_to_char(byte_idx.into()); 139 | 140 | // discard all space 141 | let mut iter = slice.chars_at(i); 142 | let mut count = 0; 143 | i -= loop { 144 | match iter.prev() { 145 | Some(c) if c.is_whitespace() => count += 1, 146 | _ => break count, 147 | } 148 | }; 149 | 150 | // if multi puctionation, skip to new non puctuation char 151 | let mut iter = slice.chars_at(i); 152 | let mut count = 0; 153 | let fp = loop { 154 | match iter.prev() { 155 | Some(c) if WORD_BOUNDARY_PUCTUATION.contains(&c) => count += 1, 156 | _ => break count, 157 | } 158 | }; 159 | i -= fp; 160 | if i == 0 { 161 | return 0; 162 | } 163 | 164 | let current_char = slice.char(i - 1); 165 | if fp > 1 || (fp == 1 && char_type(current_char) != CharType::Other) { 166 | return slice.char_to_byte(i); 167 | } 168 | 169 | let mut iter = slice.chars_at(i); 170 | let mut count = 0; 171 | i -= loop { 172 | match iter.prev() { 173 | Some(c) if !is_boundary(c, current_char) => count += 1, 174 | _ => break count, 175 | } 176 | }; 177 | 178 | slice.char_to_byte(i) 179 | } 180 | 181 | pub fn word_start>(slice: &RopeSlice, byte_idx: U) -> usize { 182 | let byte_idx = byte_idx.into(); 183 | if byte_idx >= slice.len_bytes() { 184 | return slice.len_bytes() 185 | } 186 | let mut i: usize = slice.byte_to_char(byte_idx.into()); 187 | let current_char = slice.char(i); 188 | i += slice.chars_at(i).take_while(|c| !is_boundary(*c, current_char)).count(); 189 | slice.char_to_byte(i) 190 | } 191 | 192 | pub fn word_end>(slice: &RopeSlice, byte_idx: U) -> usize { 193 | let byte_idx = byte_idx.into(); 194 | if byte_idx >= slice.len_bytes() { 195 | return slice.len_bytes() 196 | } 197 | let mut i: usize = slice.byte_to_char(byte_idx); 198 | let current_char = slice.char(i); 199 | let mut iter = slice.chars_at(i); 200 | let mut count = 0; 201 | i -= loop { 202 | match iter.prev() { 203 | Some(c) if !is_boundary(c, current_char) => count += 1, 204 | _ => break count, 205 | } 206 | }; 207 | slice.char_to_byte(i) 208 | } 209 | 210 | 211 | pub fn column_to_relative(col: Column, line: Line, buffer: &Buffer) -> Relative { 212 | let mut c = 0; 213 | let mut i = Relative::from(0); 214 | let a = line.start(buffer); 215 | while c < col.index && i < line.byte_len(buffer) { 216 | let ch = buffer.char(a + i); 217 | match ch { 218 | ' ' => { 219 | c += 1; 220 | i += 1; 221 | } 222 | '\t' => { 223 | let nb_space = buffer.tabsize - c % buffer.tabsize; 224 | c += nb_space; 225 | i += 1; 226 | } 227 | _ => { 228 | i = next_grapheme_boundary(&buffer.line_slice(line), i).into(); 229 | c += 1; 230 | } 231 | } 232 | } 233 | i 234 | } 235 | 236 | pub fn relative_to_column(relative: Relative, line: Line, buffer: &Buffer) -> Column { 237 | let mut c = Column::from(0); 238 | let mut i = Relative::from(0); 239 | let a = line.start(buffer); // Absolute::from(rope.line_to_byte(line.index)); 240 | while i < relative { 241 | let ch = buffer.char(a + i); 242 | match ch { 243 | ' ' => { 244 | c += 1; 245 | i += 1; 246 | } 247 | '\t' => { 248 | let nb_space = buffer.tabsize - c.index % buffer.tabsize; 249 | c += nb_space; 250 | i += 1; 251 | } 252 | _ => { 253 | i = next_grapheme_boundary(&buffer.line_slice(line), i).into(); 254 | c += 1; 255 | } 256 | } 257 | } 258 | c 259 | } 260 | -------------------------------------------------------------------------------- /src/widgets/text_buffer/syntax.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use ropey::Rope; 3 | use std::{ 4 | ops::{Deref, Range}, 5 | sync::{Arc, Mutex}, 6 | }; 7 | use syntect::{ 8 | highlighting::{HighlightState, Highlighter, RangedHighlightIterator, Style}, 9 | parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet}, 10 | }; 11 | 12 | use crate::theme::THEME; 13 | 14 | pub static SYNTAXSET: Lazy = Lazy::new(SyntaxSet::load_defaults_newlines); 15 | 16 | #[derive(Debug)] 17 | pub struct StateCache { 18 | states: Vec<(ParseState, HighlightState)>, 19 | highlighter: Highlighter<'static>, 20 | } 21 | #[derive(Debug)] 22 | pub struct SpanStyle { 23 | pub style: Style, 24 | pub range: Range, 25 | } 26 | 27 | impl SpanStyle { 28 | pub fn new(style: Style, range: Range) -> Self { 29 | Self { style, range } 30 | } 31 | } 32 | 33 | #[derive(Debug)] 34 | pub struct StyledLine { 35 | styles: Vec, 36 | } 37 | 38 | impl StyledLine { 39 | pub fn new(styles: Vec) -> Self { 40 | Self { styles } 41 | } 42 | } 43 | 44 | impl Deref for StyledLine { 45 | type Target = Vec; 46 | 47 | fn deref(&self) -> &Self::Target { 48 | &self.styles 49 | } 50 | } 51 | 52 | #[derive(Debug, Clone)] 53 | pub struct StyledLinesCache { 54 | pub lines: Arc>> 55 | } 56 | 57 | impl StyledLinesCache { 58 | pub fn new() -> Self { Self { lines: Arc::new(Mutex::new(Vec::new())) } } 59 | } 60 | 61 | 62 | impl StateCache { 63 | pub fn new() -> Self { 64 | StateCache { 65 | states: Vec::new(), 66 | highlighter: Highlighter::new(&THEME.style), 67 | } 68 | } 69 | 70 | pub fn update_range( 71 | &mut self, 72 | highlighted_line: StyledLinesCache, 73 | syntax: &SyntaxReference, 74 | rope: &Rope, 75 | start: usize, 76 | end: usize, 77 | ) { 78 | // states are cached every 16 lines 79 | let start = (start >> 4).min(self.states.len()); 80 | let end = (end.min(rope.len_lines()) >> 4) + 1; 81 | 82 | self.states.truncate(start); 83 | 84 | let mut states = self.states.last().cloned().unwrap_or_else(|| { 85 | ( 86 | ParseState::new(syntax), 87 | HighlightState::new(&self.highlighter, ScopeStack::new()), 88 | ) 89 | }); 90 | 91 | for i in start << 4..(end << 4).min(rope.len_lines()) { 92 | let h = if let Some(str) = rope.line(i).as_str() { 93 | let ops = states.0.parse_line(&str, &SYNTAXSET); 94 | let h: Vec<_> = RangedHighlightIterator::new(&mut states.1, &ops, &str, &self.highlighter) 95 | .map(|h| SpanStyle::new(h.0, h.2)) 96 | .collect(); 97 | StyledLine::new(h) 98 | } else { 99 | let str = rope.line(i).to_string(); 100 | let ops = states.0.parse_line(&str, &SYNTAXSET); 101 | let h: Vec<_> = RangedHighlightIterator::new(&mut states.1, &ops, &str, &self.highlighter) 102 | .map(|h| SpanStyle::new(h.0, h.2)) 103 | .collect(); 104 | StyledLine::new(h) 105 | }; 106 | if i & 0xF == 0xF { 107 | self.states.push(states.clone()); 108 | } 109 | let mut hl = highlighted_line.lines.lock().unwrap(); 110 | if i>= hl.len() { 111 | hl.push(h); 112 | } else { 113 | hl[i] = h; 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/widgets/window.rs: -------------------------------------------------------------------------------- 1 | use std::{ffi::OsStr, path::Path}; 2 | 3 | use super::{ 4 | bottom_panel::{self, BottonPanelState}, 5 | editor_view, PaletteCommandType, PALETTE_CALLBACK, 6 | }; 7 | use super::{text_buffer::EditStack, DialogResult, PaletteBuilder, PaletteView, PaletteViewState}; 8 | use crate::commands::{self, UICommandEventHandler}; 9 | 10 | use druid::{ 11 | widget::{Flex, Label, MainAxisAlignment}, 12 | Color, Data, Env, Lens, Selector, Widget, WidgetExt, WidgetPod, 13 | }; 14 | 15 | pub(super) const RESET_HELD_STATE: Selector<()> = Selector::new("nonepad.all.reste_held_state"); 16 | 17 | pub struct NPWindow { 18 | inner: WidgetPod>, 19 | palette: WidgetPod, 20 | //in_palette: bool, 21 | } 22 | 23 | #[derive(Clone, Data, Lens)] 24 | pub struct NPWindowState { 25 | pub editor: EditStack, 26 | //pub editor2: EditStack, 27 | status: String, 28 | bottom_panel: BottonPanelState, 29 | palette_state: PaletteViewState, 30 | in_palette: bool, 31 | } 32 | 33 | impl Default for NPWindowState { 34 | fn default() -> Self { 35 | NPWindowState { 36 | editor: EditStack::default(), 37 | //editor2: EditStack::default(), 38 | status: "Untilted".to_owned(), 39 | bottom_panel: BottonPanelState::default(), 40 | palette_state: PaletteViewState::default(), 41 | in_palette: false, 42 | } 43 | } 44 | } 45 | 46 | impl NPWindowState { 47 | pub fn new() -> Self { 48 | Self { ..Default::default() } 49 | } 50 | 51 | pub fn from_file>(path: P) -> anyhow::Result { 52 | Ok(Self { 53 | editor: EditStack::from_file(&path)?, 54 | //editor2: EditStack::default(), 55 | status: path 56 | .as_ref() 57 | .file_name() 58 | .unwrap_or_default() 59 | .to_string_lossy() 60 | .to_string(), 61 | ..Default::default() 62 | }) 63 | } 64 | } 65 | impl Widget for NPWindow { 66 | fn event(&mut self, ctx: &mut druid::EventCtx, event: &druid::Event, data: &mut NPWindowState, env: &druid::Env) { 67 | commands::CommandSet.event(ctx, event, self, data); 68 | if ctx.is_handled() { 69 | return; 70 | } 71 | match event { 72 | druid::Event::KeyDown(druid::KeyEvent { 73 | key: druid::KbKey::Escape, 74 | .. 75 | }) if data.bottom_panel.is_open() && !data.in_palette => { 76 | ctx.submit_command(bottom_panel::CLOSE_BOTTOM_PANEL); 77 | ctx.set_handled(); 78 | return; 79 | } 80 | druid::Event::MouseUp(_) => ctx.submit_command(super::window::RESET_HELD_STATE), 81 | druid::Event::Command(cmd) if cmd.is(PALETTE_CALLBACK) => { 82 | let item = cmd.get_unchecked(PALETTE_CALLBACK); 83 | match &item.1 { 84 | PaletteCommandType::Window(action) => { 85 | (action)(item.0.clone(), ctx, self, data); 86 | ctx.set_handled(); 87 | return; 88 | } 89 | PaletteCommandType::DialogWindow(action) => { 90 | let dialog_result = if item.0.index == 0 { 91 | DialogResult::Ok 92 | } else { 93 | DialogResult::Cancel 94 | }; 95 | (action)(dialog_result, ctx, self, data); 96 | ctx.set_handled(); 97 | return; 98 | } 99 | _ => (), 100 | } 101 | } 102 | druid::Event::Command(cmd) if cmd.is(super::SHOW_PALETTE_FOR_WINDOW) => { 103 | data.in_palette = true; 104 | ctx.request_layout(); 105 | let input = cmd.get_unchecked(super::SHOW_PALETTE_FOR_WINDOW).clone(); 106 | self.palette.widget_mut().init( 107 | &mut data.palette_state, 108 | input.1, 109 | input.2.clone(), 110 | input.3.map(|f| PaletteCommandType::Window(f)), 111 | ); 112 | self.palette.widget_mut().take_focus(ctx); 113 | return; 114 | } 115 | druid::Event::Command(cmd) if cmd.is(super::SHOW_PALETTE_FOR_EDITOR) => { 116 | data.in_palette = true; 117 | ctx.request_layout(); 118 | let input = cmd.get_unchecked(super::SHOW_PALETTE_FOR_EDITOR).clone(); 119 | self.palette.widget_mut().init( 120 | &mut data.palette_state, 121 | input.1, 122 | input.2.clone(), 123 | input.3.map(|f| PaletteCommandType::Editor(f)), 124 | ); 125 | self.palette.widget_mut().take_focus(ctx); 126 | return; 127 | } 128 | druid::Event::Command(cmd) if cmd.is(super::SHOW_DIALOG_FOR_WINDOW) => { 129 | data.in_palette = true; 130 | ctx.request_layout(); 131 | let input = cmd.get_unchecked(super::SHOW_DIALOG_FOR_WINDOW).clone(); 132 | self.palette.widget_mut().init( 133 | &mut data.palette_state, 134 | input.1, 135 | input.2.clone(), 136 | input.3.map(|f| PaletteCommandType::DialogWindow(f)), 137 | ); 138 | self.palette.widget_mut().take_focus(ctx); 139 | return; 140 | } 141 | druid::Event::Command(cmd) if cmd.is(super::SHOW_DIALOG_FOR_EDITOR) => { 142 | data.in_palette = true; 143 | ctx.request_layout(); 144 | let input = cmd.get_unchecked(super::SHOW_DIALOG_FOR_EDITOR).clone(); 145 | self.palette.widget_mut().init( 146 | &mut data.palette_state, 147 | input.1, 148 | input.2.clone(), 149 | input.3.map(|f| PaletteCommandType::DialogEditor(f)), 150 | ); 151 | self.palette.widget_mut().take_focus(ctx); 152 | return; 153 | } 154 | druid::Event::Command(cmd) if cmd.is(super::CLOSE_PALETTE) => { 155 | // TODO: send focus to the last focused editor 156 | //ctx.submit_command(Command::new(commands::GIVE_FOCUS, (), Target::Global)); 157 | ctx.focus_prev(); 158 | data.in_palette = false; 159 | ctx.request_paint(); 160 | return; 161 | } 162 | druid::Event::WindowCloseRequested => { 163 | if data.editor.is_dirty() { 164 | ctx.set_handled(); 165 | self.dialog() 166 | .title("Discard unsaved change?") 167 | .on_select(|result, ctx, _, data| { 168 | if result == DialogResult::Ok { 169 | data.editor.reset_dirty(); 170 | ctx.submit_command(druid::commands::CLOSE_WINDOW); 171 | } 172 | }) 173 | .show(ctx); 174 | } 175 | } 176 | druid::Event::WindowDisconnected => { 177 | #[cfg(target_os = "macos")] 178 | ctx.submit_command(druid::commands::QUIT_APP); 179 | } 180 | _ => (), 181 | } 182 | if data.in_palette { 183 | self.palette.event(ctx, event, &mut data.palette_state, env); 184 | } else { 185 | self.inner.event(ctx, event, data, env); 186 | } 187 | } 188 | 189 | fn lifecycle( 190 | &mut self, 191 | ctx: &mut druid::LifeCycleCtx, 192 | event: &druid::LifeCycle, 193 | data: &NPWindowState, 194 | env: &druid::Env, 195 | ) { 196 | if event.should_propagate_to_hidden() { 197 | self.palette.lifecycle(ctx, event, &data.palette_state, env); 198 | self.inner.lifecycle(ctx, event, data, env); 199 | } else { 200 | if data.in_palette { 201 | self.palette.lifecycle(ctx, event, &data.palette_state, env); 202 | } 203 | self.inner.lifecycle(ctx, event, data, env); 204 | } 205 | } 206 | 207 | fn update(&mut self, ctx: &mut druid::UpdateCtx, old_data: &NPWindowState, data: &NPWindowState, env: &druid::Env) { 208 | if old_data.in_palette != data.in_palette { 209 | ctx.children_changed(); 210 | } 211 | self.inner.update(ctx, data, env); 212 | if data.in_palette { 213 | self.palette.update(ctx, &data.palette_state, env) 214 | } 215 | } 216 | 217 | fn layout( 218 | &mut self, 219 | ctx: &mut druid::LayoutCtx, 220 | bc: &druid::BoxConstraints, 221 | data: &NPWindowState, 222 | env: &druid::Env, 223 | ) -> druid::Size { 224 | if data.in_palette { 225 | self.inner.layout(ctx, bc, data, env); 226 | self.palette.layout(ctx, bc, &data.palette_state, env) 227 | } else { 228 | self.inner.layout(ctx, bc, data, env) 229 | } 230 | } 231 | 232 | fn paint(&mut self, ctx: &mut druid::PaintCtx, data: &NPWindowState, env: &druid::Env) { 233 | self.inner.paint(ctx, data, env); 234 | if data.in_palette { 235 | self.palette.paint(ctx, &data.palette_state, env); 236 | } 237 | } 238 | } 239 | 240 | impl NPWindow { 241 | pub fn build() -> Self { 242 | let label_left = Label::new(|data: &NPWindowState, _env: &Env| { 243 | format!( 244 | "{}{}", 245 | data.editor 246 | .filename 247 | .clone() 248 | .unwrap_or_default() 249 | .file_name() 250 | .unwrap_or_else(|| OsStr::new("[Untilted]")) 251 | .to_string_lossy() 252 | .to_string(), 253 | if data.editor.is_dirty() { "*" } else { "" } 254 | ) 255 | }) 256 | .with_text_size(12.0); 257 | 258 | let label_right = Label::new(|data: &NPWindowState, _env: &Env| { 259 | format!( 260 | "{} {} {} {} {}", 261 | data.editor.caret_display_info(), 262 | data.editor.file.indentation, 263 | data.editor.file.encoding.name(), 264 | data.editor.file.linefeed, 265 | data.editor.file.syntax.name 266 | ) 267 | }) 268 | .with_text_size(12.0); 269 | 270 | let edit = editor_view::new().lens(NPWindowState::editor); 271 | //let edit2 = editor_view::new().lens(NPWindowState::editor2); 272 | NPWindow { 273 | inner: WidgetPod::new( 274 | Flex::column() 275 | //.with_flex_child(Flex::row().with_flex_child(edit,0.5).with_flex_child(edit2,0.5).padding(2.0), 1.0) 276 | .with_flex_child(edit.padding(2.0), 1.0) 277 | .must_fill_main_axis(true) 278 | .with_child(bottom_panel::build().lens(NPWindowState::bottom_panel)) 279 | .with_child( 280 | Flex::row() 281 | .with_child(label_left.padding(2.0)) 282 | .with_flex_spacer(1.0) 283 | .with_child(label_right.padding(2.0)) 284 | .padding(1.0) 285 | .background(Color::rgb8(0x1d, 0x1e, 0x22)), // using a Painter cause a redraw every frame 286 | ) 287 | .main_axis_alignment(MainAxisAlignment::Center), 288 | ), 289 | palette: WidgetPod::new(PaletteView::new()), 290 | } 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /vscodetheme/Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "itoa" 7 | version = "1.0.5" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" 10 | 11 | [[package]] 12 | name = "nonepad-vscodetheme" 13 | version = "0.1.0" 14 | dependencies = [ 15 | "serde", 16 | "serde_json", 17 | ] 18 | 19 | [[package]] 20 | name = "proc-macro2" 21 | version = "1.0.50" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" 24 | dependencies = [ 25 | "unicode-ident", 26 | ] 27 | 28 | [[package]] 29 | name = "quote" 30 | version = "1.0.23" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" 33 | dependencies = [ 34 | "proc-macro2", 35 | ] 36 | 37 | [[package]] 38 | name = "ryu" 39 | version = "1.0.12" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" 42 | 43 | [[package]] 44 | name = "serde" 45 | version = "1.0.152" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" 48 | dependencies = [ 49 | "serde_derive", 50 | ] 51 | 52 | [[package]] 53 | name = "serde_derive" 54 | version = "1.0.152" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" 57 | dependencies = [ 58 | "proc-macro2", 59 | "quote", 60 | "syn", 61 | ] 62 | 63 | [[package]] 64 | name = "serde_json" 65 | version = "1.0.91" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" 68 | dependencies = [ 69 | "itoa", 70 | "ryu", 71 | "serde", 72 | ] 73 | 74 | [[package]] 75 | name = "syn" 76 | version = "1.0.107" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" 79 | dependencies = [ 80 | "proc-macro2", 81 | "quote", 82 | "unicode-ident", 83 | ] 84 | 85 | [[package]] 86 | name = "unicode-ident" 87 | version = "1.0.6" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" 90 | -------------------------------------------------------------------------------- /vscodetheme/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "nonepad-vscodetheme" 3 | version = "0.1.0" 4 | edition = "2021" 5 | license = "Apache-2.0" 6 | description = "Helper vscode theme library for nonepad" 7 | homepage = "https://github.com/pepone42/nonepad" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [dependencies] 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0" -------------------------------------------------------------------------------- /vscodetheme/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, marker::PhantomData}; 2 | 3 | #[rustfmt::skip] 4 | 5 | use serde::{de, Deserialize, Deserializer, Serialize}; 6 | 7 | #[allow(dead_code)] 8 | #[derive(Serialize, Deserialize, Debug)] 9 | pub struct Colors { 10 | #[serde(rename = "focusBorder")] 11 | pub focus_border: String, 12 | #[serde(rename = "foreground")] 13 | pub foreground: String, 14 | #[serde(rename = "selection.background")] 15 | pub selection_background: String, 16 | #[serde(rename = "widget.shadow")] 17 | pub widget_shadow: String, 18 | #[serde(rename = "textLink.activeForeground")] 19 | pub text_link_active_foreground: String, 20 | #[serde(rename = "textLink.foreground")] 21 | pub text_link_foreground: String, 22 | #[serde(rename = "textPreformat.foreground")] 23 | pub text_preformat_foreground: String, 24 | #[serde(rename = "button.background")] 25 | pub button_background: String, 26 | #[serde(rename = "button.foreground")] 27 | pub button_foreground: String, 28 | #[serde(rename = "button.hoverBackground")] 29 | pub button_hover_background: String, 30 | #[serde(rename = "dropdown.background")] 31 | pub dropdown_background: String, 32 | #[serde(rename = "dropdown.listBackground")] 33 | pub dropdown_list_background: String, 34 | #[serde(rename = "input.background")] 35 | pub input_background: String, 36 | #[serde(rename = "input.border")] 37 | pub input_border: String, 38 | #[serde(rename = "input.foreground")] 39 | pub input_foreground: String, 40 | #[serde(rename = "input.placeholderForeground")] 41 | pub input_placeholder_foreground: String, 42 | #[serde(rename = "scrollbar.shadow")] 43 | pub scrollbar_shadow: String, 44 | #[serde(rename = "scrollbarSlider.activeBackground")] 45 | pub scrollbar_slider_active_background: String, 46 | #[serde(rename = "scrollbarSlider.background")] 47 | pub scrollbar_slider_background: String, 48 | #[serde(rename = "scrollbarSlider.hoverBackground")] 49 | pub scrollbar_slider_hover_background: String, 50 | #[serde(rename = "badge.foreground")] 51 | pub badge_foreground: String, 52 | #[serde(rename = "badge.background")] 53 | pub badge_background: String, 54 | #[serde(rename = "progressBar.background")] 55 | pub progress_bar_background: String, 56 | #[serde(rename = "list.activeSelectionBackground")] 57 | pub list_active_selection_background: String, 58 | #[serde(rename = "list.activeSelectionForeground")] 59 | pub list_active_selection_foreground: String, 60 | #[serde(rename = "list.inactiveSelectionBackground")] 61 | pub list_inactive_selection_background: String, 62 | #[serde(rename = "list.inactiveSelectionForeground")] 63 | pub list_inactive_selection_foreground: String, 64 | #[serde(rename = "list.hoverForeground")] 65 | pub list_hover_foreground: String, 66 | #[serde(rename = "list.focusForeground")] 67 | pub list_focus_foreground: String, 68 | #[serde(rename = "list.focusBackground")] 69 | pub list_focus_background: String, 70 | #[serde(rename = "list.hoverBackground")] 71 | pub list_hover_background: String, 72 | #[serde(rename = "list.dropBackground")] 73 | pub list_drop_background: String, 74 | #[serde(rename = "list.highlightForeground")] 75 | pub list_highlight_foreground: String, 76 | #[serde(rename = "list.errorForeground")] 77 | pub list_error_foreground: String, 78 | #[serde(rename = "list.warningForeground")] 79 | pub list_warning_foreground: String, 80 | #[serde(rename = "activityBar.background")] 81 | pub activity_bar_background: String, 82 | #[serde(rename = "activityBar.dropBackground")] 83 | pub activity_bar_drop_background: String, 84 | #[serde(rename = "activityBar.foreground")] 85 | pub activity_bar_foreground: String, 86 | #[serde(rename = "activityBarBadge.background")] 87 | pub activity_bar_badge_background: String, 88 | #[serde(rename = "activityBarBadge.foreground")] 89 | pub activity_bar_badge_foreground: String, 90 | #[serde(rename = "sideBar.background")] 91 | pub side_bar_background: String, 92 | #[serde(rename = "sideBar.foreground")] 93 | pub side_bar_foreground: String, 94 | #[serde(rename = "sideBarSectionHeader.background")] 95 | pub side_bar_section_header_background: String, 96 | #[serde(rename = "sideBarSectionHeader.foreground")] 97 | pub side_bar_section_header_foreground: String, 98 | #[serde(rename = "sideBarTitle.foreground")] 99 | pub side_bar_title_foreground: String, 100 | #[serde(rename = "editorGroup.border")] 101 | pub editor_group_border: String, 102 | #[serde(rename = "editorGroup.dropBackground")] 103 | pub editor_group_drop_background: String, 104 | #[serde(rename = "editorGroupHeader.noTabsBackground")] 105 | pub editor_group_header_no_tabs_background: String, 106 | #[serde(rename = "editorGroupHeader.tabsBackground")] 107 | pub editor_group_header_tabs_background: String, 108 | #[serde(rename = "tab.activeBackground")] 109 | pub tab_active_background: String, 110 | #[serde(rename = "tab.activeForeground")] 111 | pub tab_active_foreground: String, 112 | #[serde(rename = "tab.border")] 113 | pub tab_border: String, 114 | #[serde(rename = "tab.activeBorder")] 115 | pub tab_active_border: String, 116 | #[serde(rename = "tab.unfocusedActiveBorder")] 117 | pub tab_unfocused_active_border: String, 118 | #[serde(rename = "tab.inactiveBackground")] 119 | pub tab_inactive_background: String, 120 | #[serde(rename = "tab.inactiveForeground")] 121 | pub tab_inactive_foreground: String, 122 | #[serde(rename = "tab.unfocusedActiveForeground")] 123 | pub tab_unfocused_active_foreground: String, 124 | #[serde(rename = "tab.unfocusedInactiveForeground")] 125 | pub tab_unfocused_inactive_foreground: String, 126 | #[serde(rename = "editor.background")] 127 | pub editor_background: String, 128 | #[serde(rename = "editor.foreground")] 129 | pub editor_foreground: String, 130 | #[serde(rename = "editor.hoverHighlightBackground")] 131 | pub editor_hover_highlight_background: String, 132 | #[serde(rename = "editor.findMatchBackground")] 133 | pub editor_find_match_background: String, 134 | #[serde(rename = "editor.findMatchHighlightBackground")] 135 | pub editor_find_match_highlight_background: String, 136 | #[serde(rename = "editor.findRangeHighlightBackground")] 137 | pub editor_find_range_highlight_background: String, 138 | #[serde(rename = "editor.lineHighlightBackground")] 139 | pub editor_line_highlight_background: String, 140 | #[serde(rename = "editor.lineHighlightBorder")] 141 | pub editor_line_highlight_border: String, 142 | #[serde(rename = "editor.inactiveSelectionBackground")] 143 | pub editor_inactive_selection_background: String, 144 | #[serde(rename = "editor.selectionBackground")] 145 | pub editor_selection_background: String, 146 | #[serde(rename = "editor.selectionHighlightBackground")] 147 | pub editor_selection_highlight_background: String, 148 | #[serde(rename = "editor.rangeHighlightBackground")] 149 | pub editor_range_highlight_background: String, 150 | #[serde(rename = "editor.wordHighlightBackground")] 151 | pub editor_word_highlight_background: String, 152 | #[serde(rename = "editor.wordHighlightStrongBackground")] 153 | pub editor_word_highlight_strong_background: String, 154 | #[serde(rename = "editorError.foreground")] 155 | pub editor_error_foreground: String, 156 | #[serde(rename = "editorError.border")] 157 | pub editor_error_border: String, 158 | #[serde(rename = "editorWarning.foreground")] 159 | pub editor_warning_foreground: String, 160 | #[serde(rename = "editorInfo.foreground")] 161 | pub editor_info_foreground: String, 162 | #[serde(rename = "editorWarning.border")] 163 | pub editor_warning_border: String, 164 | #[serde(rename = "editorCursor.foreground")] 165 | pub editor_cursor_foreground: String, 166 | #[serde(rename = "editorIndentGuide.background")] 167 | pub editor_indent_guide_background: String, 168 | #[serde(rename = "editorLineNumber.foreground")] 169 | pub editor_line_number_foreground: String, 170 | #[serde(rename = "editorWhitespace.foreground")] 171 | pub editor_whitespace_foreground: String, 172 | #[serde(rename = "editorOverviewRuler.border")] 173 | pub editor_overview_ruler_border: String, 174 | #[serde(rename = "editorOverviewRuler.currentContentForeground")] 175 | pub editor_overview_ruler_current_content_foreground: String, 176 | #[serde(rename = "editorOverviewRuler.incomingContentForeground")] 177 | pub editor_overview_ruler_incoming_content_foreground: String, 178 | #[serde(rename = "editorOverviewRuler.findMatchForeground")] 179 | pub editor_overview_ruler_find_match_foreground: String, 180 | #[serde(rename = "editorOverviewRuler.rangeHighlightForeground")] 181 | pub editor_overview_ruler_range_highlight_foreground: String, 182 | #[serde(rename = "editorOverviewRuler.selectionHighlightForeground")] 183 | pub editor_overview_ruler_selection_highlight_foreground: String, 184 | #[serde(rename = "editorOverviewRuler.wordHighlightForeground")] 185 | pub editor_overview_ruler_word_highlight_foreground: String, 186 | #[serde(rename = "editorOverviewRuler.wordHighlightStrongForeground")] 187 | pub editor_overview_ruler_word_highlight_strong_foreground: String, 188 | #[serde(rename = "editorOverviewRuler.modifiedForeground")] 189 | pub editor_overview_ruler_modified_foregrund: String, 190 | #[serde(rename = "editorOverviewRuler.addedForeground")] 191 | pub editor_overview_ruler_added_foreground: String, 192 | #[serde(rename = "editorOverviewRuler.deletedForeground")] 193 | pub editor_overview_ruler_deleted_foreground: String, 194 | #[serde(rename = "editorOverviewRuler.errorForeground")] 195 | pub editor_overview_ruler_error_foreground: String, 196 | #[serde(rename = "editorOverviewRuler.warningForeground")] 197 | pub editor_overview_ruler_warning_foreground: String, 198 | #[serde(rename = "editorOverviewRuler.infoForeground")] 199 | pub editor_overview_ruler_info_foreground: String, 200 | #[serde(rename = "editorOverviewRuler.bracketMatchForeground")] 201 | pub editor_overview_ruler_bracket_match_foreground: String, 202 | #[serde(rename = "editorGutter.modifiedBackground")] 203 | pub editor_gutter_modified_background: String, 204 | #[serde(rename = "editorGutter.addedBackground")] 205 | pub editor_gutter_added_background: String, 206 | #[serde(rename = "editorGutter.deletedBackground")] 207 | pub editor_gutter_deleted_background: String, 208 | #[serde(rename = "diffEditor.insertedTextBackground")] 209 | pub diff_editor_inserted_text_background: String, 210 | #[serde(rename = "diffEditor.removedTextBackground")] 211 | pub diff_editor_removed_text_background: String, 212 | #[serde(rename = "editorWidget.background")] 213 | pub editor_widget_background: String, 214 | #[serde(rename = "editorWidget.border")] 215 | pub editor_widget_border: String, 216 | #[serde(rename = "editorSuggestWidget.background")] 217 | pub editor_suggest_widget_background: String, 218 | #[serde(rename = "peekView.border")] 219 | pub peek_view_border: String, 220 | #[serde(rename = "peekViewEditor.matchHighlightBackground")] 221 | pub peek_view_editor_match_highlight_background: String, 222 | #[serde(rename = "peekViewEditorGutter.background")] 223 | pub peek_view_editor_gutter_background: String, 224 | #[serde(rename = "peekViewEditor.background")] 225 | pub peek_view_editor_background: String, 226 | #[serde(rename = "peekViewResult.background")] 227 | pub peek_view_result_background: String, 228 | #[serde(rename = "peekViewTitle.background")] 229 | pub peek_view_title_background: String, 230 | #[serde(rename = "merge.currentHeaderBackground")] 231 | pub merge_current_header_background: String, 232 | #[serde(rename = "merge.currentContentBackground")] 233 | pub merge_current_content_background: String, 234 | #[serde(rename = "merge.incomingHeaderBackground")] 235 | pub merge_incoming_header_background: String, 236 | #[serde(rename = "merge.incomingContentBackground")] 237 | pub merge_incoming_content_background: String, 238 | #[serde(rename = "panel.background")] 239 | pub panel_background: String, 240 | #[serde(rename = "panel.border")] 241 | pub panel_border: String, 242 | #[serde(rename = "panelTitle.activeBorder")] 243 | pub panel_title_active_border: String, 244 | #[serde(rename = "statusBar.background")] 245 | pub status_bar_background: String, 246 | #[serde(rename = "statusBar.debuggingBackground")] 247 | pub status_bar_debugging_background: String, 248 | #[serde(rename = "statusBar.debuggingForeground")] 249 | pub status_bar_debugging_foreground: String, 250 | #[serde(rename = "statusBar.noFolderForeground")] 251 | pub status_bar_no_folder_foreground: String, 252 | #[serde(rename = "statusBar.noFolderBackground")] 253 | pub status_bar_no_folder_background: String, 254 | #[serde(rename = "statusBar.foreground")] 255 | pub status_bar_foreground: String, 256 | #[serde(rename = "statusBarItem.activeBackground")] 257 | pub status_bar_item_active_background: String, 258 | #[serde(rename = "statusBarItem.hoverBackground")] 259 | pub status_bar_item_hover_background: String, 260 | #[serde(rename = "statusBarItem.prominentBackground")] 261 | pub status_bar_item_prominent_background: String, 262 | #[serde(rename = "statusBarItem.prominentHoverBackground")] 263 | pub status_bar_item_prominent_hover_background: String, 264 | #[serde(rename = "statusBar.border")] 265 | pub status_bar_border: String, 266 | #[serde(rename = "titleBar.activeBackground")] 267 | pub title_bar_active_background: String, 268 | #[serde(rename = "titleBar.activeForeground")] 269 | pub title_bar_active_foreground: String, 270 | #[serde(rename = "titleBar.inactiveBackground")] 271 | pub title_bar_inactive_background: String, 272 | #[serde(rename = "titleBar.inactiveForeground")] 273 | pub title_bar_inactive_foreground: String, 274 | #[serde(rename = "notificationCenterHeader.foreground")] 275 | pub notification_center_header_foreground: String, 276 | #[serde(rename = "notificationCenterHeader.background")] 277 | pub notification_center_header_background: String, 278 | #[serde(rename = "extensionButton.prominentForeground")] 279 | pub extension_button_prominent_foreground: String, 280 | #[serde(rename = "extensionButton.prominentBackground")] 281 | pub extension_button_prominent_background: String, 282 | #[serde(rename = "extensionButton.prominentHoverBackground")] 283 | pub extension_button_prominent_hover_background: String, 284 | #[serde(rename = "pickerGroup.border")] 285 | pub picker_group_border: String, 286 | #[serde(rename = "pickerGroup.foreground")] 287 | pub picker_group_foreground: String, 288 | #[serde(rename = "terminal.ansiBrightBlack")] 289 | pub terminal_ansi_bright_black: String, 290 | #[serde(rename = "terminal.ansiBlack")] 291 | pub terminal_ansi_black: String, 292 | #[serde(rename = "terminal.ansiBlue")] 293 | pub terminal_ansi_blue: String, 294 | #[serde(rename = "terminal.ansiBrightBlue")] 295 | pub terminal_ansi_bright_blue: String, 296 | #[serde(rename = "terminal.ansiBrightCyan")] 297 | pub terminal_ansi_bright_cyan: String, 298 | #[serde(rename = "terminal.ansiCyan")] 299 | pub terminal_ansi_cyan: String, 300 | #[serde(rename = "terminal.ansiBrightMagenta")] 301 | pub terminal_ansi_bright_magenta: String, 302 | #[serde(rename = "terminal.ansiMagenta")] 303 | pub terminal_ansi_magenta: String, 304 | #[serde(rename = "terminal.ansiBrightRed")] 305 | pub terminal_ansi_bright_red: String, 306 | #[serde(rename = "terminal.ansiRed")] 307 | pub terminal_ansi_red: String, 308 | #[serde(rename = "terminal.ansiYellow")] 309 | pub terminal_ansi_yellow: String, 310 | #[serde(rename = "terminal.ansiBrightYellow")] 311 | pub terminal_ansi_bright_yellow: String, 312 | #[serde(rename = "terminal.ansiBrightGreen")] 313 | pub terminal_ansi_bright_green: String, 314 | #[serde(rename = "terminal.ansiGreen")] 315 | pub terminal_ansi_green: String, 316 | #[serde(rename = "terminal.ansiWhite")] 317 | pub terminal_ansi_white: String, 318 | #[serde(rename = "terminal.selectionBackground")] 319 | pub terminal_selection_background: String, 320 | #[serde(rename = "terminalCursor.background")] 321 | pub terminal_cursor_background: String, 322 | #[serde(rename = "terminalCursor.foreground")] 323 | pub terminal_cursor_foreground: String, 324 | #[serde(rename = "gitDecoration.modifiedResourceForeground")] 325 | pub git_decoration_modified_resource_foreground: String, 326 | #[serde(rename = "gitDecoration.deletedResourceForeground")] 327 | pub git_decoration_deleted_resource_foreground: String, 328 | #[serde(rename = "gitDecoration.untrackedResourceForeground")] 329 | pub git_decoration_untracked_resource_foreground: String, 330 | #[serde(rename = "gitDecoration.conflictingResourceForeground")] 331 | pub git_decoration_conflicting_resource_foreground: String, 332 | #[serde(rename = "gitDecoration.submoduleResourceForeground")] 333 | pub git_decoration_submodule_resource_foreground: String, 334 | } 335 | 336 | #[derive(Serialize, Deserialize, Debug)] 337 | pub struct TokenSetting { 338 | pub foreground: Option, 339 | #[serde(rename = "fontStyle")] 340 | pub font_style: Option, 341 | } 342 | #[derive(Serialize, Deserialize, Debug)] 343 | pub struct TokenColors { 344 | pub name: Option, 345 | #[serde(deserialize_with = "string_or_seq_string")] 346 | pub scope: Vec, 347 | pub settings: TokenSetting, 348 | } 349 | 350 | #[derive(Serialize, Deserialize, Debug)] 351 | pub struct VSCodeTheme { 352 | pub name: String, 353 | #[serde(rename = "type")] 354 | pub theme_type: String, 355 | pub colors: Colors, 356 | #[serde(rename = "tokenColors")] 357 | pub token_colors: Vec, 358 | } 359 | 360 | 361 | impl Default for VSCodeTheme { 362 | fn default() -> Self { 363 | let s = include_str!("themes/mariana.json"); 364 | serde_json::from_str(&s).unwrap() 365 | } 366 | } 367 | 368 | fn string_or_seq_string<'de, D>(deserializer: D) -> Result, D::Error> 369 | where 370 | D: Deserializer<'de>, 371 | { 372 | struct StringOrVec(PhantomData>); 373 | 374 | impl<'de> de::Visitor<'de> for StringOrVec { 375 | type Value = Vec; 376 | 377 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 378 | formatter.write_str("string or list of strings") 379 | } 380 | 381 | fn visit_str(self, value: &str) -> Result 382 | where 383 | E: de::Error, 384 | { 385 | Ok(vec![value.to_owned()]) 386 | } 387 | 388 | fn visit_seq(self, visitor: S) -> Result 389 | where 390 | S: de::SeqAccess<'de>, 391 | { 392 | Deserialize::deserialize(de::value::SeqAccessDeserializer::new(visitor)) 393 | } 394 | } 395 | 396 | deserializer.deserialize_any(StringOrVec(PhantomData)) 397 | } 398 | -------------------------------------------------------------------------------- /vscodetheme/src/themes/mariana.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Mariana (Pro)", 3 | "type": "dark", 4 | "colors": { 5 | "focusBorder": "#d0f4ff30", 6 | "foreground": "#ffffffdd", 7 | "selection.background": "#ffffff30", 8 | "widget.shadow": "#00000040", 9 | "textLink.activeForeground": "#6699cc", 10 | "textLink.foreground": "#6699cc", 11 | "textPreformat.foreground": "#6699cc", 12 | "button.background": "#6699cc", 13 | "button.foreground": "#ffffffdd", 14 | "button.hoverBackground": "#6699cc70", 15 | "dropdown.background": "#23292e", 16 | "dropdown.listBackground": "#23292e", 17 | "input.background": "#343D46", 18 | "input.border": "#d0f4ff14", 19 | "input.foreground": "#ffffffdd", 20 | "input.placeholderForeground": "#ffffff70", 21 | "scrollbar.shadow": "#00000070", 22 | "scrollbarSlider.activeBackground": "#d0f4ff30", 23 | "scrollbarSlider.background": "#d0f4ff14", 24 | "scrollbarSlider.hoverBackground": "#d0f4ff30", 25 | "badge.foreground": "#ffffffdd", 26 | "badge.background": "#EE6A6Fcc", 27 | "progressBar.background": "#23292e", 28 | "list.activeSelectionBackground": "#d0f4ff14", 29 | "list.activeSelectionForeground": "#ffffffdd", 30 | "list.inactiveSelectionBackground": "#d0f4ff07", 31 | "list.inactiveSelectionForeground": "#ffffffdd", 32 | "list.hoverForeground": "#ffffffdd", 33 | "list.focusForeground": "#ffffffdd", 34 | "list.focusBackground": "#d0f4ff14", 35 | "list.hoverBackground": "#d0f4ff07", 36 | "list.dropBackground": "#d0f4ff14", 37 | "list.highlightForeground": "#ffffffdd", 38 | "list.errorForeground": "#EE6A6F", 39 | "list.warningForeground": "#FAB763", 40 | "activityBar.background": "#23292e", 41 | "activityBar.dropBackground": "#343D46", 42 | "activityBar.foreground": "#ffffffdd", 43 | "activityBarBadge.background": "#EE6A6Fcc", 44 | "activityBarBadge.foreground": "#ffffffdd", 45 | "sideBar.background": "#23292e", 46 | "sideBar.foreground": "#ffffffdd", 47 | "sideBarSectionHeader.background": "#1d1e22", 48 | "sideBarSectionHeader.foreground": "#ffffffdd", 49 | "sideBarTitle.foreground": "#ffffffdd", 50 | "editorGroup.border": "#00000040", 51 | "editorGroup.dropBackground": "#23292e", 52 | "editorGroupHeader.noTabsBackground": "#23292e", 53 | "editorGroupHeader.tabsBackground": "#23292e", 54 | "tab.activeBackground": "#343D46", 55 | "tab.activeForeground": "#ffffffdd", 56 | "tab.border": "#ffffff10", 57 | "tab.activeBorder": "#343D46", 58 | "tab.unfocusedActiveBorder": "#343D46", 59 | "tab.inactiveBackground": "#d0f4ff07", 60 | "tab.inactiveForeground": "#ffffff70", 61 | "tab.unfocusedActiveForeground": "#ffffff70", 62 | "tab.unfocusedInactiveForeground": "#ffffff70", 63 | "editor.background": "#343D46", 64 | "editor.foreground": "#ffffffdd", 65 | "editor.hoverHighlightBackground": "#d0f4ff14", 66 | "editor.findMatchBackground": "#d0f4ff14", 67 | "editor.findMatchHighlightBackground": "#d0f4ff14", 68 | "editor.findRangeHighlightBackground": "#d0f4ff14", 69 | "editor.lineHighlightBackground": "#d0f4ff07", 70 | "editor.lineHighlightBorder": "#d0f4ff07", 71 | "editor.inactiveSelectionBackground": "#d0f4ff30", 72 | "editor.selectionBackground": "#d0f4ff30", 73 | "editor.selectionHighlightBackground": "#d0f4ff14", 74 | "editor.rangeHighlightBackground": "#d0f4ff14", 75 | "editor.wordHighlightBackground": "#00000000", 76 | "editor.wordHighlightStrongBackground": "#00000000", 77 | "editorError.foreground": "#EE6A6F", 78 | "editorError.border": "#00000000", 79 | "editorWarning.foreground": "#FAB763", 80 | "editorInfo.foreground": "#A3CE9E", 81 | "editorWarning.border": "#00000000", 82 | "editorCursor.foreground": "#ffffffdd", 83 | "editorIndentGuide.background": "#d0f4ff14", 84 | "editorLineNumber.foreground": "#d0f4ff30", 85 | "editorWhitespace.foreground": "#d0f4ff30", 86 | "editorOverviewRuler.border": "#d0f4ff14", 87 | "editorOverviewRuler.currentContentForeground": "#d0f4ff30", 88 | "editorOverviewRuler.incomingContentForeground": "#d0f4ff30", 89 | "editorOverviewRuler.findMatchForeground": "#d0f4ff30", 90 | "editorOverviewRuler.rangeHighlightForeground": "#d0f4ff30", 91 | "editorOverviewRuler.selectionHighlightForeground": "#d0f4ff30", 92 | "editorOverviewRuler.wordHighlightForeground": "#d0f4ff30", 93 | "editorOverviewRuler.wordHighlightStrongForeground": "#d0f4ff30", 94 | "editorOverviewRuler.modifiedForeground": "#ebcb8b", 95 | "editorOverviewRuler.addedForeground": "#A3CE9E", 96 | "editorOverviewRuler.deletedForeground": "#EE6A6F", 97 | "editorOverviewRuler.errorForeground": "#EE6A6F", 98 | "editorOverviewRuler.warningForeground": "#ebcb8b", 99 | "editorOverviewRuler.infoForeground": "#d0f4ff30", 100 | "editorOverviewRuler.bracketMatchForeground": "#d0f4ff30", 101 | "editorGutter.modifiedBackground": "#ebcb8b", 102 | "editorGutter.addedBackground": "#A3CE9E", 103 | "editorGutter.deletedBackground": "#EE6A6F", 104 | "diffEditor.insertedTextBackground": "#A3CE9E40", 105 | "diffEditor.removedTextBackground": "#EE6A6F40", 106 | "editorWidget.background": "#23292e", 107 | "editorWidget.border": "#d0f4ff30", 108 | "editorSuggestWidget.background": "#23292e", 109 | "peekView.border": "#6699cc", 110 | "peekViewEditor.matchHighlightBackground": "#d0f4ff30", 111 | "peekViewEditorGutter.background": "#23292e", 112 | "peekViewEditor.background": "#23292e", 113 | "peekViewResult.background": "#343D46", 114 | "peekViewTitle.background": "#23292e", 115 | "merge.currentHeaderBackground": "#FAB76370", 116 | "merge.currentContentBackground": "#FAB76340", 117 | "merge.incomingHeaderBackground": "#A3CE9E70", 118 | "merge.incomingContentBackground": "#A3CE9E40", 119 | "panel.background": "#1d1e22", 120 | "panel.border": "#d0f4ff14", 121 | "panelTitle.activeBorder": "#6699cc", 122 | "statusBar.background": "#1d1e22", 123 | "statusBar.debuggingBackground": "#6699cc", 124 | "statusBar.debuggingForeground": "#ffffffdd", 125 | "statusBar.noFolderForeground": "#ffffffdd", 126 | "statusBar.noFolderBackground": "#d0f4ff14", 127 | "statusBar.foreground": "#ffffffdd", 128 | "statusBarItem.activeBackground": "#d0f4ff14", 129 | "statusBarItem.hoverBackground": "#d0f4ff07", 130 | "statusBarItem.prominentBackground": "#d0f4ff14", 131 | "statusBarItem.prominentHoverBackground": "#d0f4ff07", 132 | "statusBar.border": "#d0f4ff14", 133 | "titleBar.activeBackground": "#23292e", 134 | "titleBar.activeForeground": "#ffffffdd", 135 | "titleBar.inactiveBackground": "#23292e", 136 | "titleBar.inactiveForeground": "#ffffff70", 137 | "notificationCenterHeader.foreground": "#ffffffdd", 138 | "notificationCenterHeader.background": "#23292e", 139 | "extensionButton.prominentForeground": "#ffffffdd", 140 | "extensionButton.prominentBackground": "#6699cc", 141 | "extensionButton.prominentHoverBackground": "#6699cc70", 142 | "pickerGroup.border": "#6699cc", 143 | "pickerGroup.foreground": "#6699cc", 144 | "terminal.ansiBrightBlack": "#ffffffdd", 145 | "terminal.ansiBlack": "#ffffffdd", 146 | "terminal.ansiBlue": "#6699cc", 147 | "terminal.ansiBrightBlue": "#6699cc", 148 | "terminal.ansiBrightCyan": "#6699cc", 149 | "terminal.ansiCyan": "#6699cc", 150 | "terminal.ansiBrightMagenta": "#c594c5", 151 | "terminal.ansiMagenta": "#c594c5", 152 | "terminal.ansiBrightRed": "#EE6A6F", 153 | "terminal.ansiRed": "#EE6A6F", 154 | "terminal.ansiYellow": "#FAB763", 155 | "terminal.ansiBrightYellow": "#FAB763", 156 | "terminal.ansiBrightGreen": "#A3CE9E", 157 | "terminal.ansiGreen": "#A3CE9E", 158 | "terminal.ansiWhite": "#ffffffdd", 159 | "terminal.selectionBackground": "#ffffff30", 160 | "terminalCursor.background": "#ffffff30", 161 | "terminalCursor.foreground": "#ffffffdd", 162 | "gitDecoration.modifiedResourceForeground": "#ebcb8b", 163 | "gitDecoration.deletedResourceForeground": "#EE6A6F", 164 | "gitDecoration.untrackedResourceForeground": "#ffffff70", 165 | "gitDecoration.conflictingResourceForeground": "#EE6A6F", 166 | "gitDecoration.submoduleResourceForeground": "#ffffff70" 167 | }, 168 | "tokenColors": [ 169 | { 170 | "name": "Comment", 171 | "scope": [ 172 | "comment", 173 | "punctuation.definition.comment" 174 | ], 175 | "settings": { 176 | "foreground": "#ffffff70" 177 | } 178 | }, 179 | { 180 | "name": "String", 181 | "scope": "string", 182 | "settings": { 183 | "foreground": "#A3CE9E" 184 | } 185 | }, 186 | { 187 | "name": "Punctuation", 188 | "scope": [ 189 | "punctuation.definition" 190 | ], 191 | "settings": { 192 | "foreground": "#5fb3b3" 193 | } 194 | }, 195 | { 196 | "name": "Number", 197 | "scope": "constant.numeric", 198 | "settings": { 199 | "foreground": "#FAB763" 200 | } 201 | }, 202 | { 203 | "name": "Built-in constant", 204 | "scope": "constant.language", 205 | "settings": { 206 | "fontStyle": "italic", 207 | "foreground": "#EE6A6F" 208 | } 209 | }, 210 | { 211 | "name": "User-defined constant", 212 | "scope": [ 213 | "constant.character", 214 | "constant.other" 215 | ], 216 | "settings": { 217 | "foreground": "#c594c5" 218 | } 219 | }, 220 | { 221 | "name": "Member Variable", 222 | "scope": "variable.member", 223 | "settings": { 224 | "foreground": "#EE6A6F" 225 | } 226 | }, 227 | { 228 | "name": "Keyword", 229 | "scope": [ 230 | "keyword", 231 | "keyword.operator.word", 232 | "keyword.control", 233 | "keyword.operator.new.js" 234 | ], 235 | "settings": { 236 | "foreground": "#c594c5" 237 | } 238 | }, 239 | { 240 | "name": "Embedded Line Template Line JS", 241 | "scope": "string.template.js meta.template.expression.js meta.embedded.line.js", 242 | "settings": { 243 | "foreground": "#ffffffdd" 244 | } 245 | }, 246 | { 247 | "name": "Operators", 248 | "scope": "keyword.operator", 249 | "settings": { 250 | "foreground": "#FA8763" 251 | } 252 | }, 253 | { 254 | "name": "Punctuation", 255 | "scope": [ 256 | "punctuation.separator", 257 | "punctuation.terminator" 258 | ], 259 | "settings": { 260 | "foreground": "#ffffff70" 261 | } 262 | }, 263 | { 264 | "name": "Punctuation", 265 | "scope": "punctuation.section", 266 | "settings": { 267 | "foreground": "#ffffffdd" 268 | } 269 | }, 270 | { 271 | "name": "Accessor", 272 | "scope": "punctuation.accessor", 273 | "settings": { 274 | "foreground": "#ffffff70" 275 | } 276 | }, 277 | { 278 | "name": "Annotation Punctuation", 279 | "scope": "punctuation.definition.annotation", 280 | "settings": { 281 | "foreground": "#5fb3b3" 282 | } 283 | }, 284 | { 285 | "name": "JavaScript Dollar", 286 | "scope": [ 287 | "variable.other.dollar.only.js", 288 | "variable.other.object.dollar.only.js", 289 | "variable.type.dollar.only.js", 290 | "support.class.dollar.only.js" 291 | ], 292 | "settings": { 293 | "foreground": "#5fb3b3" 294 | } 295 | }, 296 | { 297 | "name": "Storage", 298 | "scope": "storage", 299 | "settings": { 300 | "foreground": "#EE6A6F" 301 | } 302 | }, 303 | { 304 | "name": "Storage type", 305 | "scope": "storage.type", 306 | "settings": { 307 | "fontStyle": "italic", 308 | "foreground": "#c594c5" 309 | } 310 | }, 311 | { 312 | "name": "Entity name", 313 | "scope": "entity.name.function", 314 | "settings": { 315 | "foreground": "#5fb3b3" 316 | } 317 | }, 318 | { 319 | "name": "Object Property JS", 320 | "scope": "meta.object-literal.key.js", 321 | "settings": { 322 | "foreground": "#6699cc" 323 | } 324 | }, 325 | { 326 | "name": "Entity name", 327 | "scope": "entity.name", 328 | "settings": { 329 | "foreground": "#FAB763" 330 | } 331 | }, 332 | { 333 | "name": "Inherited class", 334 | "scope": "entity.other.inherited-class", 335 | "settings": { 336 | "fontStyle": "italic underline", 337 | "foreground": "#5fb3b3" 338 | } 339 | }, 340 | { 341 | "name": "Function argument", 342 | "scope": "variable.parameter", 343 | "settings": { 344 | "fontStyle": "", 345 | "foreground": "#FAB763" 346 | } 347 | }, 348 | { 349 | "name": "Language variable", 350 | "scope": "variable.language", 351 | "settings": { 352 | "fontStyle": "italic", 353 | "foreground": "#EE6A6F" 354 | } 355 | }, 356 | { 357 | "name": "Tag name", 358 | "scope": "entity.name.tag", 359 | "settings": { 360 | "fontStyle": "", 361 | "foreground": "#EE6A6F" 362 | } 363 | }, 364 | { 365 | "name": "Tag attribute", 366 | "scope": "entity.other.attribute-name", 367 | "settings": { 368 | "foreground": "#c594c5" 369 | } 370 | }, 371 | { 372 | "name": "Function call", 373 | "scope": [ 374 | "variable.function", 375 | "variable.annotation" 376 | ], 377 | "settings": { 378 | "fontStyle": "", 379 | "foreground": "#6699cc" 380 | } 381 | }, 382 | { 383 | "name": "Library function", 384 | "scope": [ 385 | "support.function", 386 | "support.macro" 387 | ], 388 | "settings": { 389 | "fontStyle": "italic", 390 | "foreground": "#6699cc" 391 | } 392 | }, 393 | { 394 | "name": "Library constant", 395 | "scope": "support.constant", 396 | "settings": { 397 | "fontStyle": "italic", 398 | "foreground": "#c594c5" 399 | } 400 | }, 401 | { 402 | "name": "Library class/type", 403 | "scope": [ 404 | "support.type", 405 | "support.class" 406 | ], 407 | "settings": { 408 | "foreground": "#6699cc" 409 | } 410 | }, 411 | { 412 | "name": "Invalid", 413 | "scope": "invalid", 414 | "settings": { 415 | "foreground": "#ffffff70" 416 | } 417 | }, 418 | { 419 | "name": "Invalid deprecated", 420 | "scope": "invalid.deprecated", 421 | "settings": { 422 | "foreground": "#ffffffdd" 423 | } 424 | }, 425 | { 426 | "name": "YAML Key", 427 | "scope": "entity.name.tag.yaml", 428 | "settings": { 429 | "foreground": "#5fb3b3" 430 | } 431 | }, 432 | { 433 | "name": "YAML String", 434 | "scope": "source.yaml string.unquoted", 435 | "settings": { 436 | "foreground": "#ffffffdd" 437 | } 438 | }, 439 | { 440 | "name": "markup headings", 441 | "scope": "markup.heading", 442 | "settings": { 443 | "fontStyle": "bold" 444 | } 445 | }, 446 | { 447 | "name": "markup headings", 448 | "scope": "markup.heading punctuation.definition.heading", 449 | "settings": { 450 | "foreground": "#EE6A6F" 451 | } 452 | }, 453 | { 454 | "name": "markup h1", 455 | "scope": "markup.heading.1 punctuation.definition.heading", 456 | "settings": { 457 | "foreground": "#EE6A6F" 458 | } 459 | }, 460 | { 461 | "name": "markup links", 462 | "scope": [ 463 | "string.other.link", 464 | "markup.underline.link" 465 | ], 466 | "settings": { 467 | "foreground": "#6699cc" 468 | } 469 | }, 470 | { 471 | "name": "markup bold", 472 | "scope": "markup.bold", 473 | "settings": { 474 | "fontStyle": "bold" 475 | } 476 | }, 477 | { 478 | "name": "markup italic", 479 | "scope": "markup.italic", 480 | "settings": { 481 | "fontStyle": "italic" 482 | } 483 | }, 484 | { 485 | "name": "markup bold/italic", 486 | "scope": "markup.italic markup.bold | markup.bold markup.italic", 487 | "settings": { 488 | "fontStyle": "bold italic" 489 | } 490 | }, 491 | { 492 | "name": "markup hr", 493 | "scope": "punctuation.definition.thematic-break", 494 | "settings": { 495 | "foreground": "#FAB763" 496 | } 497 | }, 498 | { 499 | "name": "markup numbered list bullet", 500 | "scope": "markup.list.numbered.bullet", 501 | "settings": { 502 | "foreground": "#A3CE9E" 503 | } 504 | }, 505 | { 506 | "name": "markup blockquote", 507 | "scope": [ 508 | "markup.quote punctuation.definition.blockquote", 509 | "markup.list punctuation.definition.list_item" 510 | ], 511 | "settings": { 512 | "foreground": "#FAB763" 513 | } 514 | }, 515 | { 516 | "name": "markup punctuation", 517 | "scope": "(text punctuation.definition.italic | text punctuation.definition.bold)", 518 | "settings": { 519 | "foreground": "#c594c5" 520 | } 521 | }, 522 | { 523 | "name": "diff.header", 524 | "scope": [ 525 | "meta.diff", 526 | "meta.diff.header" 527 | ], 528 | "settings": { 529 | "foreground": "#c594c5" 530 | } 531 | }, 532 | { 533 | "name": "diff.deleted", 534 | "scope": "markup.deleted", 535 | "settings": { 536 | "foreground": "#EE6A6F" 537 | } 538 | }, 539 | { 540 | "name": "diff.inserted", 541 | "scope": "markup.inserted", 542 | "settings": { 543 | "foreground": "#A3CE9E" 544 | } 545 | }, 546 | { 547 | "name": "diff.changed", 548 | "scope": "markup.changed", 549 | "settings": { 550 | "foreground": "#ebcb8b" 551 | } 552 | }, 553 | { 554 | "name": "CSS Properties", 555 | "scope": "support.type.property-name", 556 | "settings": { 557 | "fontStyle": "", 558 | "foreground": "#ffffffdd" 559 | } 560 | }, 561 | { 562 | "scope": "constant.numeric.line-number.match", 563 | "settings": { 564 | "foreground": "#EE6A6F" 565 | } 566 | }, 567 | { 568 | "scope": "message.error", 569 | "settings": { 570 | "foreground": "#EE6A6F" 571 | } 572 | } 573 | ] 574 | } --------------------------------------------------------------------------------