diff options
author | jgmdev <jgmdev@gmail.com> | 2022-11-01 20:57:07 -0400 |
---|---|---|
committer | jgmdev <jgmdev@gmail.com> | 2022-11-01 20:57:07 -0400 |
commit | 49139e03398c9d0ecb347267a4882a4eb3f7ee23 (patch) | |
tree | 6ac890a209172a29e09ad47ccd0156e7f7114382 | |
parent | 380f6ef5fe9f8af19cd1f6b4c043eede51cbfcae (diff) | |
parent | 0971a7a686a4e18ee31b576c460966a5ec20ff01 (diff) | |
download | lite-xl-plugins-49139e03398c9d0ecb347267a4882a4eb3f7ee23.tar.gz lite-xl-plugins-49139e03398c9d0ecb347267a4882a4eb3f7ee23.zip |
Merge branch '2.1'
128 files changed, 6767 insertions, 2047 deletions
@@ -2,180 +2,178 @@ Plugins for the [Lite XL text editor](https://github.com/lite-xl/lite-xl), originally forked from the [lite plugins repository](https://github.com/rxi/lite-plugins). -If you can't find a plugin that suits your needs, -check if someone has already created an issue about it, -otherwise feel free to create one yourself. +If you can't find a plugin that suits your needs, check if someone has already created an issue about it, otherwise feel free to create one yourself. ## How to install To install a plugin: -* If the plugin links to a repository, follow its `README`. -* If the plugin is a single file, drop it directly in: - * Linux `~/.config/lite-xl/plugins/` - * MacOS `~/.config/lite-xl/plugins/` - * Windows `C:\Users\(username)\.config\lite-xl\plugins\` +* If the plugin links to a repository, follow its `README`. +* If the plugin is a single file, drop it directly in: -*Note: if you make a pull request, the table should be updated and kept in -alphabetical order. If your plugin is large (or you'd otherwise prefer it to -have its own repo), the table can simply be updated to add a link to the repo; -otherwise the plugin file itself can be submitted. If a plugin's link resolves -to something other than a raw file it should be marked with an asterisk.* + * Linux `~/.config/lite-xl/plugins/` + * MacOS `~/.config/lite-xl/plugins/` + * Windows `C:\Users\(username)\.config\lite-xl\plugins\` -*** +_Note: if you make a pull request, the table should be updated and kept in alphabetical order. If your plugin is large (or you'd otherwise prefer it to have its own repo), the table can simply be updated to add a link to the repo; otherwise the plugin file itself can be submitted. If a plugin's link resolves to something other than a raw file it should be marked with an asterisk._ -| Plugin | Description | -| -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [`align_carets`](plugins/align_carets.lua?raw=1) | Align multiple carets and selections *([clip](https://user-images.githubusercontent.com/2798487/165631951-532f8d24-d596-4dd0-9d21-ff53c71ed32f.mp4))* | -| [`autoinsert`](plugins/autoinsert.lua?raw=1) | Automatically inserts closing brackets and quotes. Also allows selected text to be wrapped with brackets or quotes. | -| [`autosave`](plugins/autosave.lua?raw=1) | Automatically saves files when they are changed | -| [`autosaveonfocuslost`](plugins/autosaveonfocuslost.lua?raw=1) | Automatically saves files that were changed when the main window loses focus by switching to another application | -| [`autowrap`](plugins/autowrap.lua?raw=1) | Automatically hardwraps lines when typing | -| [`bigclock`](plugins/bigclock.lua?raw=1) | Shows the current time and date in a view with large text *([screenshot](https://user-images.githubusercontent.com/3920290/82752891-3318df00-9db9-11ea-803f-261d80d5cf53.png))* | -| [`black`](https://git.sr.ht/\~tmpod/black-lite)\* | Integrates the [black](https://github.com/psf/black) Python formatter with lite | -| [`bracketmatch`](plugins/bracketmatch.lua?raw=1) | Underlines matching pair for bracket under the caret *([screenshot](https://user-images.githubusercontent.com/3920290/80132745-0c863f00-8594-11ea-8875-c455c6fd7eae.png))* | -| [`centerdoc`](plugins/centerdoc.lua?raw=1) | Centers document's content on the screen *([screenshot](https://user-images.githubusercontent.com/3920290/82127896-bf6e4500-97ae-11ea-97fc-ba9a552bc9a4.png))* | -| [`colorpreview`](plugins/colorpreview.lua?raw=1) | Underlays color values (eg. `#ff00ff` or `rgb(255, 0, 255)`) with their resultant color. *([screenshot](https://user-images.githubusercontent.com/3920290/80743752-731bd780-8b15-11ea-97d3-847db927c5dc.png))* | -| [`console`](https://github.com/lite-xl/console/tree/2.0)\* | A console for running external commands and capturing their output *([gif](https://user-images.githubusercontent.com/3920290/81343656-49325a00-90ad-11ea-8647-ff39d8f1d730.gif))* | -| [`contextmenu`](https://github.com/takase1121/lite-contextmenu)\* | Simple context menu *([screenshot](https://github.com/takase1121/lite-contextmenu/blob/master/assets/screenshot.jpg?raw=true))* | -| [`copyfilelocation`](plugins/copyfilelocation.lua?raw=1) | Copy file location to clipboard | -| [`datetimestamps`](plugins/datetimestamps.lua?raw=1) | Insert date-, time- and date-time-stamps | -| [`discord-presence`](https://github.com/vincens2005/lite-xl-discord)\* | Adds the current workspace and file to your Discord Rich Presence | -| [`dragdropselected`](plugins/dragdropselected.lua?raw=1) | Provides basic drag and drop of selected text (in same document) | -| [`eofnewline`](https://github.com/bokunodev/lite_modules/blob/master/plugins/eofnewline-xl.lua?raw=1) | Make sure the file ends with one blank line. | -| [`ephemeral_tabs`](plugins/ephemeral_tabs.lua?raw=1) | Preview tabs. Opening a doc will replace the contents of the preview tab. Marks tabs as non-preview on any change or tab double clicking. | -| [`equationgrapher`](https://github.com/ThaCuber/equationgrapher?raw=1)\* | Graphs y=x equations. | -| [`eval`](plugins/eval.lua?raw=1) | Replaces selected Lua code with its evaluated result | -| [`exec`](plugins/exec.lua?raw=1) | Runs selected text through shell command and replaces with result | -| [`extend_selection_line`](plugins/extend_selection_line.lua?raw=1) | When a selection crosses multiple lines, it is drawn to the end of the screen *([screenshot](https://user-images.githubusercontent.com/2798487/140995616-89a20b55-5917-4df8-8a7c-d7c53732fa8b.png))* | -| [`fallbackfonts`](https://github.com/takase1121/lite-fallback-fonts)\* | Adds support for fallback fonts *([gif](https://raw.githubusercontent.com/takase1121/lite-fallback-fonts/master/assets/Iw18fI57J0.gif))* | -| [`fontconfig`](plugins/fontconfig.lua?raw=1) | Allows users to load fonts with [fontconfig](https://www.freedesktop.org/software/fontconfig/fontconfig-user.html). | -| [`force_syntax`](plugins/force_syntax.lua?raw=1) | Change the syntax used for a file. | -| [`formatter`](https://github.com/vincens2005/lite-formatters)\* | formatters for various languages | -| [`ghmarkdown`](plugins/ghmarkdown.lua?raw=1) | Opens a preview of the current markdown file in a browser window *([screenshot](https://user-images.githubusercontent.com/3920290/82754898-f7394600-9dc7-11ea-8278-2305363ed372.png))* | -| [`gitdiff_highlight`](https://github.com/vincens2005/lite-xl-gitdiff-highlight)\* | highlight changed lines from git *([screenshot](https://raw.githubusercontent.com/vincens2005/lite-xl-gitdiff-highlight/master/screenshot.png))* | -| [`gitstatus`](plugins/gitstatus.lua?raw=1) | Displays git branch and insert/delete count in status bar *([screenshot](https://user-images.githubusercontent.com/3920290/81107223-bcea3080-8f0e-11ea-8fc7-d03173f42e33.png))* | -| [`gofmt`](plugins/gofmt.lua?raw=1) | Auto-formats the current go file, adds the missing imports and the missing return cases | -| [`hidelinenumbers`](plugins/hidelinenumbers.lua?raw=1) | Hides the line numbers on the left of documents *([screenshot](https://user-images.githubusercontent.com/3920290/81692043-b8b19c00-9455-11ea-8d74-ad99be4b9c5f.png))* | -| [`hidestatus`](plugins/hidestatus.lua?raw=1) | Hides the status bar at the bottom of the window | -| [`immersive-title`](https://github.com/takase1121/lite-xl-immersive-title)\* | Dark (or even Mica!) title bar for Lite XL | -| [`indent_convert`](plugins/indent_convert.lua?raw=1) | Convert between tabs and spaces indentation | -| [`indentguide`](plugins/indentguide.lua?raw=1) | Adds indent guides *([screenshot](https://user-images.githubusercontent.com/3920290/79640716-f9860000-818a-11ea-9c3b-26d10dd0e0c0.png))* | -| [`Kinc Projects`](https://github.com/Kode-Community/kinc_plugin)\* | Adds [Kinc](https://github.com/Kode/Kinc) Project generation with basic build commands(depends on [`console`](https://github.com/franko/console)) | -| [`language_angelscript`](plugins/language_angelscript.lua?raw=1) | Syntax for the [Angelscript](https://www.angelcode.com/angelscript/) programming language | -| [`language_assembly_x86`](plugins/language_assembly_x86.lua?raw=1) | Syntax for Intel x86 assembly | -| [`language_autohotkey`](https://github.com/devPnal/lite-plugin-autohotkey)\* | Syntax for the [AutoHotkey](https://www.autohotkey.com) programming language | -| [`language_batch`](plugins/language_batch.lua?raw=1) | Syntax for Windows [Batch Files](https://en.wikipedia.org/wiki/Batch_file) | -| [`language_bib`](plugins/language_bib.lua?raw=1) | Syntax for [BibTex](https://en.wikipedia.org/wiki/BibTeX) files | -| [`language_cmake`](plugins/language_cmake.lua?raw=1) | Syntax for the CMake build system language | -| [`language_containerfile`](https://github.com/FilBot3/lite-xl-language-containerfile)\* | Syntax for [Containerfile](https://github.com/containers/common/blob/main/docs/Containerfile.5.md)/[Dockerfile](https://docs.docker.com/engine/reference/builder/) | -| [`language_cpp`](plugins/language_cpp.lua?raw=1) | Syntax for the [C++](https://isocpp.org/) programming language | -| [`language_crystal`](https://github.com/Tamnac/lite-plugin-crystal)\* | Syntax for the [Crystal](https://crystal-lang.org) programming language | -| [`language_csharp`](plugins/language_csharp.lua?raw=1) | Syntax for the [C#](http://csharp.net) programming language | -| [`language_d`](plugins/language_d.lua?raw=1) | Syntax for the [D](https://dlang.org/) programming language | -| [`language_dart`](plugins/language_dart.lua?raw=1) | Syntax for the [Dart](https://dart.dev/) programming languiage | -| [`language_diff`](plugins/language_diff.lua?raw=1) | Syntax for diff and patch files | -| [`language_elixir`](plugins/language_elixir.lua?raw=1) | Syntax for the [Elixir](https://elixir-lang.org) programming language | -| [`language_elm`](plugins/language_elm.lua?raw=1) | Syntax for the [Elm](https://elm-lang.org) programming language | -| [`language_env`](https://github.com/anthonyaxenov/lite-xl-env-syntax)\* | Syntax for the [env](https://hexdocs.pm/dotenvy/dotenv-file-format.html) (dotenv) files | -| [`language_erb`](plugins/language_erb.lua?raw=1) | Syntax for the [ERB](https://github.com/ruby/erb) programming language. Also known as eRuby or Embedded Ruby. | -| [`language_fe`](plugins/language_fe.lua?raw=1) | Syntax for the [fe](https://github.com/rxi/fe) programming language | -| [`language_fennel`](plugins/language_fennel.lua?raw=1) | Syntax for the [fennel](https://fennel-lang.org) programming language | -| [`language_fstab`](plugins/language_fstab.lua?raw=1) | Syntax for the [fstab](https://en.wikipedia.org/wiki/Fstab) config files | -| [`language_gdscript`](plugins/language_gdscript.lua?raw=1) | Syntax for the [Godot Engine](https://godotengine.org/)'s GDScript scripting language | -| [`language_glsl`](plugins/language_glsl.lua?raw=1) | Syntax for the [GLSL](https://www.khronos.org/registry/OpenGL/specs/gl/) programming language | -| [`language_gmi`](plugins/language_gmi.lua?raw=1) | Syntax for the [Gemtext](https://gemini.circumlunar.space/docs/gemtext.gmi) markup language | -| [`language_go`](plugins/language_go.lua?raw=1) | Syntax for the [Go](https://golang.org/) programming language | -| [`language_hlsl`](plugins/language_hlsl.lua?raw=1) | Syntax for the [HLSL](https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl) programming language | -| [`language_hs`](plugins/language_hs.lua?raw=1) | Syntax for the [Haskell](https://www.haskell.org/) programming language | -| [`language_htaccess`](plugins/language_htaccess.lua?raw=1) | Syntax for [htaccess](https://en.wikipedia.org/wiki/.htaccess) files | -| [`language_ignore`](https://github.com/anthonyaxenov/lite-xl-ignore-syntax)\* | Syntax for [.gitignore](https://git-scm.com/docs/gitignore), [.dockerignore](https://docs.docker.com/engine/reference/builder/#dockerignore-file) and some other `.*ignore` files | -| [`language_ini`](plugins/language_ini.lua?raw=1) | Syntax for [ini](https://en.wikipedia.org/wiki/INI_file) files | -| [`language_java`](plugins/language_java.lua?raw=1) | Syntax for the [Java](https://en.wikipedia.org/wiki/Java_\(programming_language\)) programming language | -| [`language_jiyu`](plugins/language_jiyu.lua?raw=1) | Syntax for the [jiyu](https://github.com/machinamentum/jiyu) programming language | -| [`language_jsx`](plugins/language_jsx.lua?raw=1) | Syntax for the [JSX](https://reactjs.org/docs/introducing-jsx.html) language for the React framework in JavaScript | -| [`language_julia`](plugins/language_julia.lua?raw=1) | Syntax for the [Julia](https://julialang.org/) programming language | -| [`language_ksy`](https://raw.githubusercontent.com/whiteh0le/lite-plugins/main/plugins/language_ksy.lua?raw=1) | Syntax for [Kaitai](http://kaitai.io/) struct files | -| [`language_liquid`](plugins/language_liquid.lua?raw=1) | Syntax for [Liquid](https://shopify.github.io/liquid/) templating language | -| [`language_lobster`](plugins/language_lobster.lua?raw=1) | Syntax for [Lobster](https://strlen.com/lobster/) programming language | -| [`language_make`](plugins/language_make.lua?raw=1) | Syntax for the Make build system language | -| [`language_meson`](plugins/language_meson.lua?raw=1) | Syntax for the [Meson](https://mesonbuild.com) build system language | -| [`language_moon`](plugins/language_moon.lua?raw=1) | Syntax for the [MoonScript](https://moonscript.org) scripting language | -| [`language_nelua`](https://github.com/AKDev21/nelua-lite-xl)\* | Syntax for [Nelua](http://nelua.io/) programming | -| [`language_nginx`](plugins/language_nginx.lua?raw=1) | Syntax for [Nginx](https://www.nginx.com/) config files | -| [`language_nim`](plugins/language_nim.lua?raw=1) | Syntax for the [Nim](https://nim-lang.org) programming language | -| [`language_objc`](plugins/language_objc.lua?raw=1) | Syntax for the [Objective C](https://en.wikipedia.org/wiki/Objective-C) programming language | -| [`language_odin`](plugins/language_odin.lua?raw=1) | Syntax for the [Odin](https://github.com/odin-lang/Odin) programming language | -| [`language_perl`](plugins/language_perl.lua?raw=1) | Syntax for the [Perl](https://perl.org) programming language | -| [`language_php`](plugins/language_php.lua?raw=1) | Syntax for the [PHP](https://php.net) programming language | -| [`language_pico8`](plugins/language_pico8.lua?raw=1) | Syntax for [Pico-8](https://www.lexaloffle.com/pico-8.php) cartridge files | -| [`language_pkgbuild`](plugins/language_pkgbuild.lua?raw=1) | Syntax for [PKGBUILD](https://wiki.archlinux.org/title/PKGBUILD) package description files | -| [`language_po`](plugins/language_po.lua?raw=1) | Syntax for [PO](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) translation files | -| [`language_pony`](https://github.com/MrAnyx/lite-plugin-pony)\* | Syntax for [Pony](https://www.ponylang.io/) programming language | -| [`language_powershell`](plugins/language_powershell.lua?raw=1) | Syntax for [PowerShell](https://docs.microsoft.com/en-us/powershell) scripting language | -| [`language_psql`](plugins/language_psql.lua?raw=1) | Syntax for the postgresql database access language | -| [`language_r`](plugins/language_R.lua?raw=1) | Syntax for [R](https://www.r-project.org/) scripting language | -| [`language_rescript`](plugins/language_rescript.lua?raw=1) | Syntax for the [ReScript](https://rescript-lang.org/) programming language | -| [`language_rivet`](plugins/language_rivet.lua?raw=1) | Syntax for the [Rivet](https://github.com/rivet-lang/rivet) programming language | -| [`language_ruby`](plugins/language_ruby.lua?raw=1) | Syntax for the [Ruby](https://www.ruby-lang.org/) programming language | -| [`language_rust`](plugins/language_rust.lua?raw=1) | Syntax for the [Rust](https://rust-lang.org/) programming language | -| [`language_sass`](plugins/language_sass.lua?raw=1) | Syntax for the [Sass](https://sass-lang.com/) CSS preprocessor | -| [`language_scala`](plugins/language_scala.lua?raw=1) | Syntax for the [Scala](https://scala-lang.org/) programming language | -| [`language_sh`](plugins/language_sh.lua?raw=1) | Syntax for shell scripting language | -| [`language_ssh_config`](plugins/language_ssh_config.lua?raw=1) | Syntax for ssh & sshd config files | -| [`language_tcl`](plugins/language_tcl.lua?raw=1) | Syntax for the [Tcl](https://www.tcl.tk/) programming language | -| [`language_teal`](plugins/language_teal.lua?raw=1) | Syntax for the [Teal](https://github.com/teal-language/tl) programming language, a typed dialect of Lua. | -| [`language_tex`](plugins/language_tex.lua?raw=1) | Syntax for the [LaTeX](https://www.latex-project.org/) typesetting language | -| [`language_toml`](plugins/language_toml.lua?raw=1) | Syntax for the [TOML](https://toml.io/en/) configuration language | -| [`language_ts`](plugins/language_ts.lua?raw=1) | Syntax for the [TypeScript](https://www.typescriptlang.org/) programming language, a typed dialect of JavaScript. | -| [`language_tsx`](plugins/language_tsx.lua?raw=1) | Syntax for [TSX](https://www.typescriptlang.org/docs/handbook/jsx.html) language | -| [`language_v`](plugins/language_v.lua?raw=1) | Syntax for the [V](https://vlang.io/) programming language | -| [`language_wren`](plugins/language_wren.lua?raw=1) | Syntax for the [Wren](http://wren.io/) programming language | -| [`language_yaml`](plugins/language_yaml.lua?raw=1) | Syntax for [YAML](https://yaml.org/) serialization language | -| [`language_zig`](plugins/language_zig.lua?raw=1) | Syntax for the [Zig](https://ziglang.org/) programming language | -| [`lfautoinsert`](plugins/lfautoinsert.lua?raw=1) | Automatically inserts indentation and closing bracket/text after newline | -| [`linecopypaste`](plugins/linecopypaste.lua?raw=1) | Copy, cut and paste the current line when nothing is selected | -| [`linenumbers`](plugins/linenumbers.lua?raw=1) | The ability to change the display of the line number *([screenshot](https://user-images.githubusercontent.com/5556081/129493788-6a4cbd7a-9074-4133-bab7-110ed55f18f7.png))* | -| [`lint+`](https://github.com/liquid600pgm/lintplus)\* | Advanced linter with ErrorLens-like error reporting. Compatible with linters made for `linter` *([screenshot](https://raw.githubusercontent.com/liquid600pgm/lintplus/master/screenshots/1.png))* | -| [`linter`](https://github.com/drmargarido/linters)\* | Linters for multiple languages | -| [`litepresence`](https://github.com/TorchedSammy/Litepresence)\* | Discord rich presence for Lite XL (display file editing in Discord) | -| [`lorem`](https://github.com/sheetcoder/lorem)\* | Generates Lorem Ipsum placeholder dummy text | -| [`lsp`](https://github.com/lite-xl/lite-xl-lsp)\* | Provides code completion (also known as IntelliSense) using the Language Server Protocol | -| [`lspkind`](https://github.com/TorchedSammy/lite-xl-lspkind)\* | Completion menu kind/type icons for Lite XL LSP | -| [`macmodkeys`](plugins/macmodkeys.lua?raw=1) | Remaps mac modkeys `command/option` to `ctrl/alt` | -| [`markers`](plugins/markers.lua?raw=1) | Add markers to docs and jump between them quickly *([screenshot](https://user-images.githubusercontent.com/3920290/82252149-5faaa200-9946-11ea-9199-bea2efb7ee23.png))* | -| [`memoryusage`](plugins/memoryusage.lua?raw=1) | Show memory usage in the status view | -| [`minimap`](plugins/minimap.lua?raw=1) | Shows a minimap on the right-hand side of the docview. Taken from [@andsve](https://github.com/andsve/lite-plugins/tree/minimap-plugin), and improved upon. | -| [`motiontrail`](plugins/motiontrail.lua?raw=1) | Adds a motion-trail to the caret *([gif](https://user-images.githubusercontent.com/3920290/83256814-085ccb00-a1ab-11ea-9e35-e6633cbed1a9.gif))* | -| [`navigate`](plugins/navigate.lua?raw=1) | Allows moving back and forward between document positions, reducing the amount of scrolling | -| [`nonicons`](plugins/nonicons.lua?raw=1) | File icons set for TreeView. Download [font](https://github.com/yamatsum/nonicons/raw/6a2faf4fbdfbe353c5ae6a496740ac4bfb6d0e74/dist/nonicons.ttf) to your config/fonts folder | -| [`opacity`](plugins/opacity.lua?raw=1) | Change the opaqueness/transparency of `lite-xl` using shift+mousewheel or a command. | -| [`open_ext`](plugins/open_ext.lua?raw=1) | Automatically prompts you if you tried to open a binary file in the editor | -| [`openfilelocation`](plugins/openfilelocation.lua?raw=1) | Opens the parent directory of the current file in the file manager | -| [`openselected`](plugins/openselected.lua?raw=1) | Opens the selected filename or url | -| [`pdfview`](plugins/pdfview.lua?raw=1) | PDF preview for TeX files | -| [`rainbowparen`](plugins/rainbowparen.lua?raw=1) | Show nesting of parentheses with rainbow colours | -| [`regexreplacepreview`](plugins/regexreplacepreview.lua?raw=1) | Allows for you to write a regex and its replacement in one go, and live preview the results. | -| [`restoretabs`](plugins/restoretabs.lua?raw=1) | Keep a list of recently closed tabs, and restore the tab in order on cntrl+shift+t. | -| [`scalestatus`](plugins/scalestatus.lua?raw=1) | Displays current scale (zoom) in status view (depends on scale plugin) | -| [`select_colorscheme`](plugins/select_colorscheme.lua?raw=1) | Select a color theme, like VScode, Sublime Text.(plugin saves changes) | -| [`selectionhighlight`](plugins/selectionhighlight.lua?raw=1) | Highlights regions of code that match the current selection *([screenshot](https://user-images.githubusercontent.com/3920290/80710883-5f597c80-8ae7-11ea-97f0-76dfacc08439.png))* | -| [`smallclock`](plugins/smallclock.lua?raw=1) | Displays the current time in the corner of the status view | -| [`smoothcaret`](plugins/smoothcaret.lua?raw=1) | Smooth caret animation *([gif](https://user-images.githubusercontent.com/20792268/139006049-a0ba6559-88cb-49a7-8077-4822445b4a1f.gif))* | -| [`sort`](plugins/sort.lua?raw=1) | Sorts selected lines alphabetically | -| [`spellcheck`](plugins/spellcheck.lua?raw=1) | Underlines misspelt words *([screenshot](https://user-images.githubusercontent.com/3920290/79923973-9caa7400-842e-11ea-85d4-7a196a91ca50.png))* *— note: on Windows a [`words.txt`](https://github.com/dwyl/english-words/blob/master/words.txt) dictionary file must be placed beside the exe* | -| [`statusclock`](plugins/statusclock.lua?raw=1) | Displays the current date and time in the corner of the status view | -| [`tabnumbers`](plugins/tabnumbers.lua?raw=1) | Displays tab numbers from 1–9 next to their names \*([screenshot](https://user-images.githubusercontent.com/16415678/101285362-007a8500-37e5-11eb-869b-c10eb9d9d902.png)) | -| [`texcompile`](plugins/texcompile.lua?raw=1) | Compile Tex files into PDF | -| [`theme16`](https://github.com/monolifed/theme16)\* | Theme manager with base16 themes | -| [`themeselect`](plugins/themeselect.lua?raw=1) | Select a theme based on filename of active document | -| [`titleize`](plugins/titleize.lua?raw=1) | Titleizes selected string (`hello world` => `Hello World`) | -| [`todotreeview`](https://github.com/drmargarido/TodoTreeView)\* | Todo tree viewer for annotations in code like `TODO`, `BUG`, `FIX`, `IMPROVEMENT` | -| [`togglesnakecamel`](plugins/togglesnakecamel.lua?raw=1) | Toggles symbols between `snake_case` and `camelCase` | -| [`treeview-menu-extender`](https://github.com/juliardi/lite-xl-treeview-menu-extender)\* | Extend Lite XL's treeview menu *([screenshot](https://raw.githubusercontent.com/juliardi/lite-xl-treeview-menu-extender/main/screenshot.png))* | -| [`typingspeed`](plugins/typingspeed.lua?raw=1) | Displays your current typing speed in characters and words per minute in the status bar | -| [`unboundedscroll`](plugins/unboundedscroll.lua?raw=1) | Allows scrolling outside the bounds of a document | -| [`updatechecker`](https://github.com/vincens2005/lite-xl-updatechecker)\* | Automatically checks for updates and notifies you | -| [`vibe`](https://github.com/eugenpt/lite-xl-vibe)\* | VI(vim?) bindings with a hint of DOOM Emacs, for lite-xl | -| [`visu`](https://github.com/TorchedSammy/Visu)\* | Audio visualizer for Lite XL | -| [`widget`](https://github.com/lite-xl/lite-xl-widgets)\* | Plugin library that provides a set of re-usable components to more easily write UI elements for your plugins | +--- + +| Plugin | Description | +| :------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`align_carets`](plugins/align_carets.lua?raw=1) | Align multiple carets and selections _([clip](https://user-images.githubusercontent.com/2798487/165631951-532f8d24-d596-4dd0-9d21-ff53c71ed32f.mp4))_ | +| [`autoinsert`](plugins/autoinsert.lua?raw=1) | Automatically inserts closing brackets and quotes. Also allows selected text to be wrapped with brackets or quotes. | +| [`autosave`](plugins/autosave.lua?raw=1) | Automatically saves files when they are changed | +| [`autosaveonfocuslost`](plugins/autosaveonfocuslost.lua?raw=1) | Automatically saves files that were changed when the main window loses focus by switching to another application | +| [`autowrap`](plugins/autowrap.lua?raw=1) | Automatically hardwraps lines when typing | +| [`bigclock`](plugins/bigclock.lua?raw=1) | Shows the current time and date in a view with large text _([screenshot](https://user-images.githubusercontent.com/3920290/82752891-3318df00-9db9-11ea-803f-261d80d5cf53.png))_ | +| [`black`](https://git.sr.ht/\~tmpod/black-lite)\* | Integrates the [black](https://github.com/psf/black) Python formatter with lite | +| [`bracketmatch`](plugins/bracketmatch.lua?raw=1) | Underlines matching pair for bracket under the caret _([screenshot](https://user-images.githubusercontent.com/3920290/80132745-0c863f00-8594-11ea-8875-c455c6fd7eae.png))_ | +| [`centerdoc`](plugins/centerdoc.lua?raw=1) | Centers document's content on the screen and adds zen mode support _([screenshot](https://user-images.githubusercontent.com/3920290/82127896-bf6e4500-97ae-11ea-97fc-ba9a552bc9a4.png))_ | +| [`colorpreview`](plugins/colorpreview.lua?raw=1) | Underlays color values (eg. `#ff00ff` or `rgb(255, 0, 255)`) with their resultant color. _([screenshot](https://user-images.githubusercontent.com/3920290/80743752-731bd780-8b15-11ea-97d3-847db927c5dc.png))_ | +| [`console`](https://github.com/franko/console)\* | A console for running external commands and capturing their output _([gif](https://user-images.githubusercontent.com/3920290/81343656-49325a00-90ad-11ea-8647-ff39d8f1d730.gif))_ | +| [`exterm`](https://github.com/ShadiestGoat/lite-xl-exterm)\* | Allows to open an external console in current project directory | +| [`contextmenu`](https://github.com/takase1121/lite-contextmenu)\* | Simple context menu _([screenshot](https://github.com/takase1121/lite-contextmenu/blob/master/assets/screenshot.jpg?raw=true))_ | +| [`copyfilelocation`](plugins/copyfilelocation.lua?raw=1) | Copy file location to clipboard | +| [`datetimestamps`](plugins/datetimestamps.lua?raw=1) | Insert date-, time- and date-time-stamps | +| [`discord-presence`](https://github.com/vincens2005/lite-xl-discord)\* | Adds the current workspace and file to your Discord Rich Presence | +| [`dragdropselected`](plugins/dragdropselected.lua?raw=1) | Provides basic drag and drop of selected text (in same document) | +| [`eofnewline`](https://github.com/bokunodev/lite_modules/blob/master/plugins/eofnewline-xl.lua?raw=1) | Make sure the file ends with one blank line. | +| [`ephemeral_tabs`](plugins/ephemeral_tabs.lua?raw=1) | Preview tabs. Opening a doc will replace the contents of the preview tab. Marks tabs as non-preview on any change or tab double clicking. | +| [`equationgrapher`](https://github.com/ThaCuber/equationgrapher?raw=1)\* | Graphs y=x equations. | +| [`eval`](plugins/eval.lua?raw=1) | Replaces selected Lua code with its evaluated result | +| [`exec`](plugins/exec.lua?raw=1) | Runs selected text through shell command and replaces with result | +| [`extend_selection_line`](plugins/extend_selection_line.lua?raw=1) | When a selection crosses multiple lines, it is drawn to the end of the screen _([screenshot](https://user-images.githubusercontent.com/2798487/140995616-89a20b55-5917-4df8-8a7c-d7c53732fa8b.png))_ | +| [`fallbackfonts`](https://github.com/takase1121/lite-fallback-fonts)\* | Adds support for fallback fonts _([gif](https://raw.githubusercontent.com/takase1121/lite-fallback-fonts/master/assets/Iw18fI57J0.gif))_ | +| [`fontconfig`](plugins/fontconfig.lua?raw=1) | Allows users to load fonts with [fontconfig](https://www.freedesktop.org/software/fontconfig/fontconfig-user.html). | +| [`force_syntax`](plugins/force_syntax.lua?raw=1) | Change the syntax used for a file. | +| [`formatter`](https://github.com/vincens2005/lite-formatters)\* | formatters for various languages | +| [`ghmarkdown`](plugins/ghmarkdown.lua?raw=1) | Opens a preview of the current markdown file in a browser window _([screenshot](https://user-images.githubusercontent.com/3920290/82754898-f7394600-9dc7-11ea-8278-2305363ed372.png))_ | +| [`gitdiff_highlight`](https://github.com/vincens2005/lite-xl-gitdiff-highlight)\* | highlight changed lines from git _([screenshot](https://raw.githubusercontent.com/vincens2005/lite-xl-gitdiff-highlight/master/screenshot.png))_ | +| [`gitstatus`](plugins/gitstatus.lua?raw=1) | Displays git branch and insert/delete count in status bar _([screenshot](https://user-images.githubusercontent.com/3920290/81107223-bcea3080-8f0e-11ea-8fc7-d03173f42e33.png))_ | +| [`gofmt`](plugins/gofmt.lua?raw=1) | Auto-formats the current go file, adds the missing imports and the missing return cases | +| [`immersive-title`](https://github.com/takase1121/lite-xl-immersive-title)\* | Dark (or even Mica!) title bar for Lite XL | +| [`indent_convert`](plugins/indent_convert.lua?raw=1) | Convert between tabs and spaces indentation | +| [`indentguide`](plugins/indentguide.lua?raw=1) | Adds indent guides _([screenshot](https://user-images.githubusercontent.com/3920290/79640716-f9860000-818a-11ea-9c3b-26d10dd0e0c0.png))_ | +| [`ipc`](plugins/ipc.lua?raw=1) | Adds inter-process communication support | +| [`Kinc Projects`](https://github.com/Kode-Community/kinc_plugin)\* | Adds [Kinc](https://github.com/Kode/Kinc) Project generation with basic build commands(depends on [`console`](https://github.com/franko/console)) | +| [`language_angelscript`](plugins/language_angelscript.lua?raw=1) | Syntax for the [Angelscript](https://www.angelcode.com/angelscript/) programming language | +| [`language_assembly_x86`](plugins/language_assembly_x86.lua?raw=1) | Syntax for Intel x86 assembly | +| [`language_autohotkey`](https://github.com/devPnal/lite-plugin-autohotkey)\* | Syntax for the [AutoHotkey](https://www.autohotkey.com) programming language | +| [`language_batch`](plugins/language_batch.lua?raw=1) | Syntax for Windows [Batch Files](https://en.wikipedia.org/wiki/Batch_file) | +| [`language_bib`](plugins/language_bib.lua?raw=1) | Syntax for [BibTex](https://en.wikipedia.org/wiki/BibTeX) files | +| [`language_caddyfile`](plugins/language_caddyfile.lua?raw=1) | Syntax for the Caddyfile used on the [Caddy](https://caddyserver.com/) web server | +| [`language_cmake`](plugins/language_cmake.lua?raw=1) | Syntax for the CMake build system language | +| [`language_containerfile`](https://github.com/FilBot3/lite-xl-language-containerfile)\* | Syntax for [Containerfile](https://github.com/containers/common/blob/main/docs/Containerfile.5.md)/[Dockerfile](https://docs.docker.com/engine/reference/builder/) | +| [`language_crystal`](https://github.com/Tamnac/lite-plugin-crystal)\* | Syntax for the [Crystal](https://crystal-lang.org) programming language | +| [`language_csharp`](plugins/language_csharp.lua?raw=1) | Syntax for the [C#](http://csharp.net) programming language | +| [`language_d`](plugins/language_d.lua?raw=1) | Syntax for the [D](https://dlang.org/) programming language | +| [`language_dart`](plugins/language_dart.lua?raw=1) | Syntax for the [Dart](https://dart.dev/) programming languiage | +| [`language_diff`](plugins/language_diff.lua?raw=1) | Syntax for diff and patch files | +| [`language_elixir`](plugins/language_elixir.lua?raw=1) | Syntax for the [Elixir](https://elixir-lang.org) programming language | +| [`language_elm`](plugins/language_elm.lua?raw=1) | Syntax for the [Elm](https://elm-lang.org) programming language | +| [`language_erb`](plugins/language_erb.lua?raw=1) | Syntax for the [ERB](https://github.com/ruby/erb) programming language. Also known as eRuby or Embedded Ruby. | +| [`language_env`](https://github.com/anthonyaxenov/lite-xl-env-syntax)\* | Syntax for the [env](https://hexdocs.pm/dotenvy/dotenv-file-format.html) (dotenv) files | +| [`language_fe`](plugins/language_fe.lua?raw=1) | Syntax for the [fe](https://github.com/rxi/fe) programming language | +| [`language_fennel`](plugins/language_fennel.lua?raw=1) | Syntax for the [fennel](https://fennel-lang.org) programming language | +| [`language_fstab`](plugins/language_fstab.lua?raw=1) | Syntax for the [fstab](https://en.wikipedia.org/wiki/Fstab) config files | +| [`language_gdscript`](plugins/language_gdscript.lua?raw=1) | Syntax for the [Godot Engine](https://godotengine.org/)'s GDScript scripting language | +| [`language_glsl`](plugins/language_glsl.lua?raw=1) | Syntax for the [GLSL](https://www.khronos.org/registry/OpenGL/specs/gl/) programming language | +| [`language_gmi`](plugins/language_gmi.lua?raw=1) | Syntax for the [Gemtext](https://gemini.circumlunar.space/docs/gemtext.gmi) markup language | +| [`language_go`](plugins/language_go.lua?raw=1) | Syntax for the [Go](https://golang.org/) programming language | +| [`language_hlsl`](plugins/language_hlsl.lua?raw=1) | Syntax for the [HLSL](https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl) programming language | +| [`language_hs`](plugins/language_hs.lua?raw=1) | Syntax for the [Haskell](https://www.haskell.org/) programming language | +| [`language_ini`](plugins/language_ini.lua?raw=1) | Syntax for [ini](https://en.wikipedia.org/wiki/INI_file) files | +| [`language_ignore`](https://github.com/anthonyaxenov/lite-xl-ignore-syntax)\* | Syntax for [.gitignore](https://git-scm.com/docs/gitignore), [.dockerignore](https://docs.docker.com/engine/reference/builder/#dockerignore-file) and some other `.*ignore` files | +| [`language_java`](plugins/language_java.lua?raw=1) | Syntax for the [Java](https://en.wikipedia.org/wiki/Java_\(programming_language\)) programming language | +| [`language_jiyu`](plugins/language_jiyu.lua?raw=1) | Syntax for the [jiyu](https://github.com/machinamentum/jiyu) programming language | +| [`language_jsx`](plugins/language_jsx.lua?raw=1) | Syntax for the [JSX](https://reactjs.org/docs/introducing-jsx.html) language for the React framework in JavaScript | +| [`language_julia`](plugins/language_julia.lua?raw=1) | Syntax for the [Julia](https://julialang.org/) programming language | +| [`language_ksy`](https://raw.githubusercontent.com/whiteh0le/lite-plugins/main/plugins/language_ksy.lua?raw=1) | Syntax for [Kaitai](http://kaitai.io/) struct files | +| [`language_liquid`](plugins/language_liquid.lua?raw=1) | Syntax for [Liquid](https://shopify.github.io/liquid/) templating language | +| [`language_lobster`](plugins/language_lobster.lua?raw=1) | Syntax for [Lobster](https://strlen.com/lobster/) programming language | +| [`language_make`](plugins/language_make.lua?raw=1) | Syntax for the Make build system language | +| [`language_meson`](plugins/language_meson.lua?raw=1) | Syntax for the [Meson](https://mesonbuild.com) build system language | +| [`language_miniscript`](plugins/language_miniscript.lua?raw=1) | Syntax for the [MiniScript](https://miniscript.org) programming language | +| [`language_moon`](plugins/language_moon.lua?raw=1) | Syntax for the [MoonScript](https://moonscript.org) scripting language | +| [`language_nelua`](https://github.com/AKDev21/nelua-lite-xl)\* | Syntax for [Nelua](http://nelua.io/) programming | +| [`language_nginx`](plugins/language_nginx.lua?raw=1) | Syntax for [Nginx](https://www.nginx.com/) config files | +| [`language_nim`](plugins/language_nim.lua?raw=1) | Syntax for the [Nim](https://nim-lang.org) programming language | +| [`language_objc`](plugins/language_objc.lua?raw=1) | Syntax for the [Objective C](https://en.wikipedia.org/wiki/Objective-C) programming language | +| [`language_odin`](plugins/language_odin.lua?raw=1) | Syntax for the [Odin](https://github.com/odin-lang/Odin) programming language | +| [`language_perl`](plugins/language_perl.lua?raw=1) | Syntax for the [Perl](https://perl.org) programming language | +| [`language_php`](plugins/language_php.lua?raw=1) | Syntax for the [PHP](https://php.net) programming language | +| [`language_pico8`](plugins/language_pico8.lua?raw=1) | Syntax for [Pico-8](https://www.lexaloffle.com/pico-8.php) cartridge files | +| [`language_pkgbuild`](plugins/language_pkgbuild.lua?raw=1) | Syntax for [PKGBUILD](https://wiki.archlinux.org/title/PKGBUILD) package description files | +| [`language_po`](plugins/language_po.lua?raw=1) | Syntax for [PO](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) translation files | +| [`language_pony`](https://github.com/MrAnyx/lite-plugin-pony)\* | Syntax for [Pony](https://www.ponylang.io/) programming language | +| [`language_powershell`](plugins/language_powershell.lua?raw=1) | Syntax for [PowerShell](https://docs.microsoft.com/en-us/powershell) scripting language | +| [`language_psql`](plugins/language_psql.lua?raw=1) | Syntax for the postgresql database access language | +| [`language_r`](plugins/language_R.lua?raw=1) | Syntax for [R](https://www.r-project.org/) scripting language | +| [`language_rescript`](plugins/language_rescript.lua?raw=1) | Syntax for the [ReScript](https://rescript-lang.org/) programming language | +| [`language_rivet`](plugins/language_rivet.lua?raw=1) | Syntax for the [Rivet](https://github.com/rivet-lang/rivet) programming language | +| [`language_ruby`](plugins/language_ruby.lua?raw=1) | Syntax for the [Ruby](https://www.ruby-lang.org/) programming language | +| [`language_rust`](plugins/language_rust.lua?raw=1) | Syntax for the [Rust](https://rust-lang.org/) programming language | +| [`language_sass`](plugins/language_sass.lua?raw=1) | Syntax for the [Sass](https://sass-lang.com/) CSS preprocessor | +| [`language_scala`](plugins/language_scala.lua?raw=1) | Syntax for the [Scala](https://scala-lang.org/) programming language | +| [`language_sh`](plugins/language_sh.lua?raw=1) | Syntax for shell scripting language | +| [`language_ssh_config`](plugins/language_ssh_config.lua?raw=1) | Syntax for ssh & sshd config files | +| [`language_tcl`](plugins/language_tcl.lua?raw=1) | Syntax for the [Tcl](https://www.tcl.tk/) programming language | +| [`language_teal`](plugins/language_teal.lua?raw=1) | Syntax for the [Teal](https://github.com/teal-language/tl) programming language, a typed dialect of Lua. | +| [`language_tex`](plugins/language_tex.lua?raw=1) | Syntax for the [LaTeX](https://www.latex-project.org/) typesetting language | +| [`language_toml`](plugins/language_toml.lua?raw=1) | Syntax for the [TOML](https://toml.io/en/) configuration language | +| [`language_ts`](plugins/language_ts.lua?raw=1) | Syntax for the [TypeScript](https://www.typescriptlang.org/) programming language, a typed dialect of JavaScript. | +| [`language_tsx`](plugins/language_tsx.lua?raw=1) | Syntax for [TSX](https://www.typescriptlang.org/docs/handbook/jsx.html) language | +| [`language_v`](plugins/language_v.lua?raw=1) | Syntax for the [V](https://vlang.io/) programming language | +| [`language_wren`](plugins/language_wren.lua?raw=1) | Syntax for the [Wren](http://wren.io/) programming language | +| [`language_yaml`](plugins/language_yaml.lua?raw=1) | Syntax for [YAML](https://yaml.org/) serialization language | +| [`language_zig`](plugins/language_zig.lua?raw=1) | Syntax for the [Zig](https://ziglang.org/) programming language | +| [`lfautoinsert`](plugins/lfautoinsert.lua?raw=1) | Automatically inserts indentation and closing bracket/text after newline | +| [`linenumbers`](plugins/linenumbers.lua?raw=1) | The ability to change the display of the line number _([screenshot](https://user-images.githubusercontent.com/5556081/129493788-6a4cbd7a-9074-4133-bab7-110ed55f18f7.png))_ | +| [`lint+`](https://github.com/liquid600pgm/lintplus)\* | Advanced linter with ErrorLens-like error reporting. Compatible with linters made for `linter` _([screenshot](https://raw.githubusercontent.com/liquid600pgm/lintplus/master/screenshots/1.png))_ | +| [`linter`](https://github.com/drmargarido/linters)\* | Linters for multiple languages | +| [`litepresence`](https://github.com/TorchedSammy/Litepresence)\* | Discord rich presence for Lite XL (display file editing in Discord) | +| [`lorem`](https://github.com/sheetcoder/lorem)\* | Generates Lorem Ipsum placeholder dummy text | +| [`lsp`](https://github.com/lite-xl/lite-xl-lsp)\* | Provides code completion (also known as IntelliSense) using the Language Server Protocol | +| [`lspkind`](https://github.com/TorchedSammy/lite-xl-lspkind)\* | Completion menu kind/type icons for Lite XL LSP | +| [`macmodkeys`](plugins/macmodkeys.lua?raw=1) | Remaps mac modkeys `command/option` to `ctrl/alt` | +| [`markers`](plugins/markers.lua?raw=1) | Add markers to docs and jump between them quickly _([screenshot](https://user-images.githubusercontent.com/3920290/82252149-5faaa200-9946-11ea-9199-bea2efb7ee23.png))_ | +| [`memoryusage`](plugins/memoryusage.lua?raw=1) | Show memory usage in the status view | +| [`minimap`](plugins/minimap.lua?raw=1) | Shows a minimap on the right-hand side of the docview. Taken from [@andsve](https://github.com/andsve/lite-plugins/tree/minimap-plugin), and improved upon. | +| [`motiontrail`](plugins/motiontrail.lua?raw=1) | Adds a motion-trail to the caret _([gif](https://user-images.githubusercontent.com/3920290/83256814-085ccb00-a1ab-11ea-9e35-e6633cbed1a9.gif))_ | +| [`navigate`](plugins/navigate.lua?raw=1) | Allows moving back and forward between document positions, reducing the amount of scrolling | +| [`nonicons`](plugins/nonicons.lua?raw=1) | File icons set for TreeView. Download [font](https://github.com/yamatsum/nonicons/raw/6a2faf4fbdfbe353c5ae6a496740ac4bfb6d0e74/dist/nonicons.ttf) to your config/fonts folder | +| [`opacity`](plugins/opacity.lua?raw=1) | Change the opaqueness/transparency of `lite-xl` using shift+mousewheel or a command. | +| [`open_ext`](plugins/open_ext.lua?raw=1) | Automatically prompts you if you tried to open a binary file in the editor | +| [`openfilelocation`](plugins/openfilelocation.lua?raw=1) | Opens the parent directory of the current file in the file manager | +| [`openselected`](plugins/openselected.lua?raw=1) | Opens the selected filename or url | +| [`pdfview`](plugins/pdfview.lua?raw=1) | PDF preview for TeX files | +| [`primary_selection`](plugins/primary_selection.lua?raw=1) | Adds middle mouse click copy/paste (primary selection). To use this plugin, `xclip` must be installed. | +| [`rainbowparen`](plugins/rainbowparen.lua?raw=1) | Show nesting of parentheses with rainbow colours | +| [`regexreplacepreview`](plugins/regexreplacepreview.lua?raw=1) | Allows for you to write a regex and its replacement in one go, and live preview the results. | +| [`restoretabs`](plugins/restoretabs.lua?raw=1) | Keep a list of recently closed tabs, and restore the tab in order on cntrl+shift+t. | +| [`scalestatus`](plugins/scalestatus.lua?raw=1) | Displays current scale (zoom) in status view (depends on scale plugin) | +| [`select_colorscheme`](plugins/select_colorscheme.lua?raw=1) | Select a color theme, like VScode, Sublime Text.(plugin saves changes) | +| [`selectionhighlight`](plugins/selectionhighlight.lua?raw=1) | Highlights regions of code that match the current selection _([screenshot](https://user-images.githubusercontent.com/3920290/80710883-5f597c80-8ae7-11ea-97f0-76dfacc08439.png))_ | +| [`settings`](plugins/settings.lua?raw=1) | Provides a GUI to manage core and plugin settings, bindings and select color theme _([video](https://user-images.githubusercontent.com/1702572/169743674-ececae24-f6b7-4ff2-bfa2-c4762cd327d9.mp4))_. (depends on [`widget`](https://github.com/lite-xl/lite-xl-widgets)) | +| [`smallclock`](plugins/smallclock.lua?raw=1) | Displays the current time in the corner of the status view | +| [`smoothcaret`](plugins/smoothcaret.lua?raw=1) | Smooth caret animation _([gif](https://user-images.githubusercontent.com/20792268/139006049-a0ba6559-88cb-49a7-8077-4822445b4a1f.gif))_ | +| [`sort`](plugins/sort.lua?raw=1) | Sorts selected lines alphabetically | +| [`spellcheck`](plugins/spellcheck.lua?raw=1) | Underlines misspelt words _([screenshot](https://user-images.githubusercontent.com/3920290/79923973-9caa7400-842e-11ea-85d4-7a196a91ca50.png))_ _-- note: on Windows a [`words.txt`](https://github.com/dwyl/english-words/blob/master/words.txt) dictionary file must be placed beside the exe_ | +| [`statusclock`](plugins/statusclock.lua?raw=1) | Displays the current date and time in the corner of the status view | +| [`tabnumbers`](plugins/tabnumbers.lua?raw=1) | Displays tab numbers from 1–9 next to their names \*([screenshot](https://user-images.githubusercontent.com/16415678/101285362-007a8500-37e5-11eb-869b-c10eb9d9d902.png)) | +| [`texcompile`](plugins/texcompile.lua?raw=1) | Compile Tex files into PDF | +| [`theme16`](https://github.com/monolifed/theme16)\* | Theme manager with base16 themes | +| [`themeselect`](plugins/themeselect.lua?raw=1) | Select a theme based on filename of active document | +| [`titleize`](plugins/titleize.lua?raw=1) | Titleizes selected string (`hello world` => `Hello World`) | +| [`treeview-menu-extender`](https://github.com/juliardi/lite-xl-treeview-menu-extender)\* | Extend Lite XL's treeview menu _([screenshot](https://raw.githubusercontent.com/juliardi/lite-xl-treeview-menu-extender/main/screenshot.png))_ | +| [`todotreeview`](https://github.com/drmargarido/TodoTreeView)\* | Todo tree viewer for annotations in code like `TODO`, `BUG`, `FIX`, `IMPROVEMENT` | +| [`togglesnakecamel`](plugins/togglesnakecamel.lua?raw=1) | Toggles symbols between `snake_case` and `camelCase` | +| [`treeview-menu-extender`](https://github.com/juliardi/lite-xl-treeview-menu-extender)\* | Extend Lite XL's treeview menu _([screenshot](https://raw.githubusercontent.com/juliardi/lite-xl-treeview-menu-extender/main/screenshot.png))_ | +| [`typingspeed`](plugins/typingspeed.lua?raw=1) | Displays your current typing speed in characters and words per minute in the status bar | +| [`unboundedscroll`](plugins/unboundedscroll.lua?raw=1) | Allows scrolling outside the bounds of a document | +| [`updatechecker`](https://github.com/vincens2005/lite-xl-updatechecker)\* | Automatically checks for updates and notifies you | +| [`vibe`](https://github.com/eugenpt/lite-xl-vibe)\* | VI(vim?) bindings with a hint of DOOM Emacs, for lite-xl | +| [`visu`](https://github.com/TorchedSammy/Visu)\* | Audio visualizer for Lite XL | +| [`widget`](https://github.com/lite-xl/lite-xl-widgets)\* | Plugin library that provides a set of re-usable components to more easily write UI elements for your plugins | +| [`wordcount`](plugins/wordcount.lua?raw=1) | Adds in a word count to the statusview. | diff --git a/plugins/align_carets.lua b/plugins/align_carets.lua index 95d64d5..2a1db2a 100644 --- a/plugins/align_carets.lua +++ b/plugins/align_carets.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local DocView = require "core.docview" diff --git a/plugins/autoinsert.lua b/plugins/autoinsert.lua index 94bcc74..c49887a 100644 --- a/plugins/autoinsert.lua +++ b/plugins/autoinsert.lua @@ -1,20 +1,21 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local translate = require "core.doc.translate" local config = require "core.config" +local common = require "core.common" local DocView = require "core.docview" local command = require "core.command" local keymap = require "core.keymap" -config.plugins.autoinsert = { map = { +config.plugins.autoinsert = common.merge({ map = { ["["] = "]", ["{"] = "}", ["("] = ")", ['"'] = '"', ["'"] = "'", ["`"] = "`", -} } +} }, config.plugins.autoinsert) local function is_closer(chr) diff --git a/plugins/autosave.lua b/plugins/autosave.lua index 9518adf..b63a6f6 100644 --- a/plugins/autosave.lua +++ b/plugins/autosave.lua @@ -1,15 +1,40 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local Doc = require "core.doc" local command = require "core.command" +local common = require "core.common" -- this is used to detect the wait time local last_keypress = os.time() -- this exists so that we don't end up with multiple copies of the loop running at once local looping = false local on_text_change = Doc.on_text_change --- the approximate amount of time, in seconds, that it takes to trigger an autosave -config.plugins.autosave = { timeout = 1 } + +config.plugins.autosave = common.merge({ + enabled = true, + -- the approximate amount of time, in seconds, that it takes to trigger an autosave + timeout = 1, + -- The config specification used by the settings gui + config_spec = { + name = "Auto Save", + { + label = "Enable", + description = "Enable or disable the auto save feature.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Timeout", + description = "Approximate amount of time in seconds it takes to trigger an autosave.", + path = "timeout", + type = "number", + default = 1, + min = 1, + max = 30 + } + } +}, config.plugins.autosave) local function loop_for_save() @@ -38,7 +63,7 @@ end function Doc:on_text_change(type) -- check if file is saved - if self.filename then + if config.plugins.autosave.enabled and self.filename then updatepress() end return on_text_change(self, type) diff --git a/plugins/autosaveonfocuslost.lua b/plugins/autosaveonfocuslost.lua index dea1e7c..1a9bd0c 100644 --- a/plugins/autosaveonfocuslost.lua +++ b/plugins/autosaveonfocuslost.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local CommandView = require "core.commandview" local DocView = require "core.docview" diff --git a/plugins/autowrap.lua b/plugins/autowrap.lua index c9dde6d..a7a9765 100644 --- a/plugins/autowrap.lua +++ b/plugins/autowrap.lua @@ -1,10 +1,32 @@ --- mod-version:2 -- lite-xl 2.0 -require "plugins.reflow" +-- mod-version:3 +local core = require "core" local config = require "core.config" local command = require "core.command" +local common = require "core.common" local DocView = require "core.docview" -config.plugins.autowrap = { files = { "%.md$", "%.txt$" } } +config.plugins.autowrap = common.merge({ + enabled = false, + files = { "%.md$", "%.txt$" }, + -- The config specification used by the settings gui + config_spec = { + name = "Auto Wrap", + { + label = "Enable", + description = "Activates text auto wrapping by default.", + path = "enabled", + type = "toggle", + default = false + }, + { + label = "Files", + description = "List of Lua patterns matching files to auto wrap.", + path = "files", + type = "list_strings", + default = { "%.md$", "%.txt$" }, + } + } +}, config.plugins.autowrap) local on_text_input = DocView.on_text_input @@ -12,6 +34,8 @@ local on_text_input = DocView.on_text_input DocView.on_text_input = function(self, ...) on_text_input(self, ...) + if not config.plugins.autowrap.enabled then return end + -- early-exit if the filename does not match a file type pattern local filename = self.doc.filename or "" local matched = false @@ -31,7 +55,17 @@ DocView.on_text_input = function(self, ...) command.perform("doc:select-lines") command.perform("reflow:reflow") command.perform("doc:move-to-next-char") - command.perform("doc:move-to-previous-char") command.perform("doc:move-to-end-of-line") end end + +command.add(nil, { + ["auto-wrap:toggle"] = function() + config.plugins.autowrap.enabled = not config.plugins.autowrap.enabled + if config.plugins.autowrap.enabled then + core.log("Auto wrap: on") + else + core.log("Auto wrap: off") + end + end +}) diff --git a/plugins/bigclock.lua b/plugins/bigclock.lua index c246df5..f3554ac 100644 --- a/plugins/bigclock.lua +++ b/plugins/bigclock.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local style = require "core.style" local command = require "core.command" @@ -7,11 +7,39 @@ local config = require "core.config" local View = require "core.view" -config.plugins.bigclock = { +config.plugins.bigclock = common.merge({ time_format = "%H:%M:%S", date_format = "%A, %d %B %Y", - scale = 1 -} + scale = 1, + -- The config specification used by the settings gui + config_spec = { + name = "Big Clock", + { + label = "Time Format", + description = "Time specification defined with Lua date/time place holders.", + path = "time_format", + type = "string", + default = "%H:%M:%S" + }, + { + label = "Date Format", + description = "Date specification defined with Lua date/time place holders.", + path = "date_format", + type = "string", + default = "%A, %d %B %Y", + }, + { + label = "Scale", + description = "Size of the clock relative to screen.", + path = "scale", + type = "number", + default = 1, + min = 0.5, + max = 3.0, + step = 0.1 + } + } +}, config.plugins.bigclock) local ClockView = View:extend() @@ -21,6 +49,7 @@ function ClockView:new() ClockView.super.new(self) self.time_text = "" self.date_text = "" + self.last_scale = 0 end @@ -30,14 +59,18 @@ end function ClockView:update_fonts() + if self.last_scale ~= config.plugins.bigclock.scale then + self.last_scale = config.plugins.bigclock.scale + else + return + end local size = math.floor(self.size.x * 0.15 / 15) * 15 * config.plugins.bigclock.scale if self.font_size ~= size then - self.time_font = renderer.font.load(DATADIR .. "/fonts/font.ttf", size) - self.date_font = renderer.font.load(DATADIR .. "/fonts/font.ttf", size * 0.3) + self.time_font = renderer.font.copy(style["font"], size) + self.date_font = renderer.font.copy(style["font"], size * 0.3) self.font_size = size collectgarbage() end - return self.font end diff --git a/plugins/bracketmatch.lua b/plugins/bracketmatch.lua index cd9fc2f..6119330 100644 --- a/plugins/bracketmatch.lua +++ b/plugins/bracketmatch.lua @@ -1,10 +1,11 @@ --- mod-version:2 -- lite-xl 2.0 +--- mod-version:3 local core = require "core" local style = require "core.style" local command = require "core.command" local keymap = require "core.keymap" local DocView = require "core.docview" local config = require "core.config" +local common = require "core.common" -- Colors can be configured as follows: -- underline color = `style.bracketmatch_color` @@ -12,12 +13,62 @@ local config = require "core.config" -- background color = `style.bracketmatch_block_color` -- frame color = `style.bracketmatch_frame_color` -config.plugins.bracketmatch = { - highligh_both = true, -- highlight the current bracket too - style = "underline", -- can be "underline", "block", "frame", "none" - color_char = false, -- color the bracket - line_size = math.ceil(1 * SCALE), -- the size of the lines used in "underline" and "frame" -} +config.plugins.bracketmatch = common.merge({ + -- highlight the current bracket too + highlight_both = true, + -- can be "underline", "block", "frame", "none" + style = "underline", + -- color the bracket + color_char = false, + -- the size of the lines used in "underline" and "frame" + line_size = math.ceil(1 * SCALE), + -- The config specification used by the settings gui + config_spec = { + name = "Bracket Match", + { + label = "Highlight Both", + description = "Highlight the current bracket too.", + path = "highlight_both", + type = "toggle", + default = true + }, + { + label = "Style", + description = "The visual indicator for pair brackets.", + path = "style", + type = "selection", + default = "underline", + values = { + {"Underline", "underline"}, + {"Block", "block"}, + {"Frame", "frame"}, + {"None", "none"} + } + }, + { + label = "Colorize Bracket", + description = "Change the color of the matching brackets.", + path = "color_char", + type = "toggle", + default = false + }, + { + label = "Line Size", + description = "Height of the underline on matching brackets.", + path = "line_size", + type = "number", + default = 1, + min = 1, + step = 1, + get_value = function(value) + return math.floor(value / SCALE) + end, + set_value = function(value) + return math.ceil(value * SCALE) + end + } + } +}, config.plugins.bracketmatch) local bracket_maps = { @@ -179,16 +230,17 @@ end local draw_line_text = DocView.draw_line_text -function DocView:draw_line_text(idx, x, y) - draw_line_text(self, idx, x, y) +function DocView:draw_line_text(line, x, y) + local lh = draw_line_text(self, line, x, y) if self.doc == state.doc and state.line2 then - if idx == state.line2 then - draw_decoration(self, x, y, idx, state.col2) + if line == state.line2 then + draw_decoration(self, x, y, line, state.col2) end - if idx == state.line and config.plugins.bracketmatch.highligh_both then - draw_decoration(self, x, y, idx, state.col + select_adj - 1) + if line == state.line and config.plugins.bracketmatch.highlight_both then + draw_decoration(self, x, y, line, state.col + select_adj - 1) end end + return lh end diff --git a/plugins/centerdoc.lua b/plugins/centerdoc.lua index 8e4f8a4..980cbed 100644 --- a/plugins/centerdoc.lua +++ b/plugins/centerdoc.lua @@ -1,21 +1,108 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local core = require "core" local config = require "core.config" +local common = require "core.common" +local command = require "core.command" +local keymap = require "core.keymap" +local treeview = require "plugins.treeview" local DocView = require "core.docview" +config.plugins.centerdoc = common.merge({ + enabled = true, + zen_mode = false +}, config.plugins.centerdoc) local draw_line_gutter = DocView.draw_line_gutter local get_gutter_width = DocView.get_gutter_width -function DocView:draw_line_gutter(idx, x, y, width) - local real_gutter_width = get_gutter_width(self) - local offset = self:get_gutter_width() - real_gutter_width * 2 - draw_line_gutter(self, idx, x + offset, y, real_gutter_width) +function DocView:draw_line_gutter(line, x, y, width) + local lh + if not config.plugins.centerdoc.enabled then + lh = draw_line_gutter(self, line, x, y, width) + else + local real_gutter_width = self:get_font():get_width(#self.doc.lines) + local offset = self:get_gutter_width() - real_gutter_width * 2 + lh = draw_line_gutter(self, line, x + offset, y, real_gutter_width) + end + return lh end function DocView:get_gutter_width() - local real_gutter_width = get_gutter_width(self) - local width = real_gutter_width + self:get_font():get_width("n") * config.line_limit - return math.max((self.size.x - width) / 2, real_gutter_width) + if not config.plugins.centerdoc.enabled then + return get_gutter_width(self) + else + local real_gutter_width, gutter_padding = get_gutter_width(self) + local width = real_gutter_width + self:get_font():get_width("n") * config.line_limit + return math.max((self.size.x - width) / 2, real_gutter_width), gutter_padding + end end + + +local previous_win_status = system.get_window_mode() +local previous_treeview_status = treeview.visible +local previous_statusbar_status = core.status_view.visible + +local function toggle_zen_mode(enabled) + config.plugins.centerdoc.zen_mode = enabled + + if config.plugins.centerdoc.zen_mode then + previous_win_status = system.get_window_mode() + previous_treeview_status = treeview.visible + previous_statusbar_status = core.status_view.visible + + config.plugins.centerdoc.enabled = true + system.set_window_mode("fullscreen") + treeview.visible = false + command.perform "status-bar:hide" + else + config.plugins.centerdoc.enabled = false + system.set_window_mode(previous_win_status) + treeview.visible = previous_treeview_status + core.status_view.visible = previous_statusbar_status + end +end + +local on_startup = true + +-- The config specification used by the settings gui +config.plugins.centerdoc.config_spec = { + name = "Center Document", + { + label = "Enable", + description = "Activates document centering by default.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Zen Mode", + description = "Activates zen mode by default.", + path = "zen_mode", + type = "toggle", + default = false, + on_apply = function(enabled) + if on_startup then + core.add_thread(function() + toggle_zen_mode(enabled) + end) + on_startup = false + else + toggle_zen_mode(enabled) + end + end + } +} + + +command.add(nil, { + ["center-doc:toggle"] = function() + config.plugins.centerdoc.enabled = not config.plugins.centerdoc.enabled + end, + ["center-doc:zen-mode-toggle"] = function() + toggle_zen_mode(not config.plugins.centerdoc.zen_mode) + end, +}) + +keymap.add { ["ctrl+alt+z"] = "center-doc:zen-mode-toggle" } diff --git a/plugins/colorpreview.lua b/plugins/colorpreview.lua index c552f07..0aa7663 100644 --- a/plugins/colorpreview.lua +++ b/plugins/colorpreview.lua @@ -1,15 +1,31 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local config = require "core.config" local common = require "core.common" local DocView = require "core.docview" +config.plugins.colorpreview = common.merge({ + enabled = true, + -- The config specification used by the settings gui + config_spec = { + name = "Color Preview", + { + label = "Enable", + description = "Enable or disable the color preview feature.", + path = "enabled", + type = "toggle", + default = true + } + } +}, config.plugins.colorpreview) + local white = { common.color "#ffffff" } local black = { common.color "#000000" } local tmp = {} -local function draw_color_previews(self, idx, x, y, ptn, base, nibbles) - local text = self.doc.lines[idx] +local function draw_color_previews(self, line, x, y, ptn, base, nibbles) + local text = self.doc.lines[line] local s, e = 0, 0 while true do @@ -35,8 +51,8 @@ local function draw_color_previews(self, idx, x, y, ptn, base, nibbles) b = b * 16 end - local x1 = x + self:get_col_x_offset(idx, s) - local x2 = x + self:get_col_x_offset(idx, e + 1) + local x1 = x + self:get_col_x_offset(line, s) + local x2 = x + self:get_col_x_offset(line, e + 1) local oy = self:get_line_text_y_offset() local text_color = math.max(r, g, b) < 128 and white or black @@ -44,7 +60,7 @@ local function draw_color_previews(self, idx, x, y, ptn, base, nibbles) local l1, _, l2, _ = self.doc:get_selection(true) - if not (self.doc:has_selection() and idx >= l1 and idx <= l2) then + if not (self.doc:has_selection() and line >= l1 and line <= l2) then renderer.draw_rect(x1, y, x2 - x1, self:get_line_height(), tmp) renderer.draw_text(self:get_font(), str, x1, y + oy, text_color) end @@ -54,9 +70,19 @@ end local draw_line_text = DocView.draw_line_text -function DocView:draw_line_text(idx, x, y) - draw_line_text(self, idx, x, y) - draw_color_previews(self, idx, x, y, "#(%x%x)(%x%x)(%x%x)(%x?%x?)%f[%W]", 16) - draw_color_previews(self, idx, x, y, "#(%x)(%x)(%x)%f[%W]", 16, true) -- support #fff css format - draw_color_previews(self, idx, x, y, "rgba?%((%d+)%D+(%d+)%D+(%d+)[%s,]-([%.%d]-)%s-%)", nil) +function DocView:draw_line_text(line, x, y) + local lh = draw_line_text(self, line, x, y) + if config.plugins.colorpreview.enabled then + draw_color_previews(self, line, x, y, + "#(%x%x)(%x%x)(%x%x)(%x?%x?)%f[%W]", + 16 + ) + -- support #fff css format + draw_color_previews(self, line, x, y, "#(%x)(%x)(%x)%f[%W]", 16, true) + draw_color_previews(self, line, x, y, + "rgba?%((%d+)%D+(%d+)%D+(%d+)[%s,]-([%.%d]-)%s-%)", + nil + ) + end + return lh end diff --git a/plugins/copyfilelocation.lua b/plugins/copyfilelocation.lua index dedc188..eb7b1a9 100644 --- a/plugins/copyfilelocation.lua +++ b/plugins/copyfilelocation.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" diff --git a/plugins/datetimestamps.lua b/plugins/datetimestamps.lua index 51d698e..f16af83 100644 --- a/plugins/datetimestamps.lua +++ b/plugins/datetimestamps.lua @@ -1,7 +1,8 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local command = require "core.command" +local common = require "core.common" --[[ Date and time format placeholders @@ -25,11 +26,36 @@ from https://www.lua.org/pil/22.1.html %y two-digit year (98) [00-99] %% the character `%´ --]] -config.plugins.datetimestamps = { - format_datestamp = "%Y%m%d" - format_datetimestamp = "%Y%m%d_%H%M%S" - format_timestamp = "%H%M%S" -} +config.plugins.datetimestamps = common.merge({ + format_datestamp = "%Y%m%d", + format_datetimestamp = "%Y%m%d_%H%M%S", + format_timestamp = "%H%M%S", + -- The config specification used by the settings gui + config_spec = { + name = "Date and Time Stamps", + { + label = "Date", + description = "Date specification defined with Lua date/time place holders.", + path = "format_datestamp", + type = "string", + default = "%Y%m%d" + }, + { + label = "Time", + description = "Time specification defined with Lua date/time place holders.", + path = "format_timestamp", + type = "string", + default = "%H%M%S" + }, + { + label = "Date and Time", + description = "Date and time specification defined with Lua date/time place holders.", + path = "format_datetimestamp", + type = "string", + default = "%Y%m%d_%H%M%S" + } + } +}, config.plugins.datetimestamps) local function datestamp() local sOut = os.date(config.plugins.datetimestamps.format_datestamp) @@ -49,6 +75,13 @@ end command.add("core.docview", { ["datetimestamps:insert-datestamp"] = datestamp, ["datetimestamps:insert-timestamp"] = timestamp, - ["datetimestamps:insert-datetimestamp"] = datetimestamp + ["datetimestamps:insert-datetimestamp"] = datetimestamp, + ["datetimestamps:insert-custom"] = function() + core.command_view:enter("Date format eg: %H:%M:%S", { + submit = function(cmd) + core.active_view.doc:text_input(os.date(cmd) or "") + end + }) + end, }) diff --git a/plugins/dragdropselected.lua b/plugins/dragdropselected.lua index 6bdb101..3c6583b 100644 --- a/plugins/dragdropselected.lua +++ b/plugins/dragdropselected.lua @@ -1,13 +1,13 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 --[[ - dragdropselected.lua - provides basic drag and drop of selected text (in same document) - version: 20200627_133351 - originally by SwissalpS - - TODO: use OS drag and drop events - TODO: change mouse cursor when duplicating - TODO: add dragging image + dragdropselected.lua + provides basic drag and drop of selected text (in same document) + version: 20200627_133351 + originally by SwissalpS + + TODO: use OS drag and drop events + TODO: change mouse cursor when duplicating + TODO: add dragging image --]] local DocView = require "core.docview" local core = require "core" @@ -22,16 +22,16 @@ local style = require "core.style" -- iSelLine2 is line number where selection ends -- iSelCol2 is column where selection ends local function isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) - if iLine < iSelLine1 then return false end - if iLine > iSelLine2 then return false end - if (iLine == iSelLine1) and (iCol < iSelCol1) then return false end - if (iLine == iSelLine2) and (iCol > iSelCol2) then return false end - return true + if iLine < iSelLine1 then return false end + if iLine > iSelLine2 then return false end + if (iLine == iSelLine1) and (iCol < iSelCol1) then return false end + if (iLine == iSelLine2) and (iCol > iSelCol2) then return false end + return true end -- isInSelection -- distance between two points local function distance(x1, y1, x2, y2) - return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2)) + return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2)) end local min_drag = style.code_font:get_width(" ") @@ -40,126 +40,138 @@ local min_drag = style.code_font:get_width(" ") local on_mouse_moved = DocView.on_mouse_moved function DocView:on_mouse_moved(x, y, ...) - local sCursor = nil - - -- make sure we only act if previously on_mouse_pressed was in selection - if self.bClickedIntoSelection and - ( -- we are already dragging or we moved enough to start dragging - not self.drag_start_loc or - distance(self.drag_start_loc[1],self.drag_start_loc[2], x, y) > min_drag - ) then - self.drag_start_loc = nil - - -- show that we are dragging something - sCursor = 'hand' - - -- calculate line and column for current mouse position - local iLine, iCol = self:resolve_screen_position(x, y) - local iSelLine1 = self.dragged_selection[1] - local iSelCol1 = self.dragged_selection[2] - local iSelLine2 = self.dragged_selection[3] - local iSelCol2 = self.dragged_selection[4] - self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) - if not isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) then - -- show cursor only if outside selection - self.doc:add_selection(iLine, iCol) - end - -- update scroll position - self:scroll_to_line(iLine, true) - end -- if previously clicked into selection + local sCursor = nil + + -- make sure we only act if previously on_mouse_pressed was in selection + if + self.bClickedIntoSelection + and + ( -- we are already dragging or we moved enough to start dragging + not self.drag_start_loc or + distance(self.drag_start_loc[1],self.drag_start_loc[2], x, y) > min_drag + ) + then + self.drag_start_loc = nil - -- hand off to 'old' on_mouse_moved() - on_mouse_moved(self, x, y, ...) - -- override cursor as needed - if sCursor then self.cursor = sCursor end + -- show that we are dragging something + sCursor = 'hand' + + -- calculate line and column for current mouse position + local iLine, iCol = self:resolve_screen_position(x, y) + local iSelLine1 = self.dragged_selection[1] + local iSelCol1 = self.dragged_selection[2] + local iSelLine2 = self.dragged_selection[3] + local iSelCol2 = self.dragged_selection[4] + self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) + if not isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) then + -- show cursor only if outside selection + self.doc:add_selection(iLine, iCol) + end + -- update scroll position + self:scroll_to_line(iLine, true) + end -- if previously clicked into selection + + -- hand off to 'old' on_mouse_moved() + on_mouse_moved(self, x, y, ...) + -- override cursor as needed + if sCursor then self.cursor = sCursor end end -- DocView:on_mouse_moved -- override DocView:on_mouse_pressed local on_mouse_pressed = DocView.on_mouse_pressed function DocView:on_mouse_pressed(button, x, y, clicks) - local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks) - if caught then - return caught - end - -- no need to proceed if not left button or has no selection - if ('left' ~= button) - or (not self.doc:has_selection()) - or (1 < clicks) then - return on_mouse_pressed(self, button, x, y, clicks) - end - -- convert pixel coordinates to line and column coordinates - local iLine, iCol = self:resolve_screen_position(x, y) - -- get selection coordinates - local iSelLine1, iSelCol1, iSelLine2, iSelCol2 = self.doc:get_selection(true) - -- set flag for on_mouse_released and on_mouse_moved() methods to detect dragging - self.bClickedIntoSelection = isInSelection(iLine, iCol, iSelLine1, iSelCol1, - iSelLine2, iSelCol2) - if self.bClickedIntoSelection then - self.drag_start_loc = { x, y } - -- stash selection for inserting later - self.sDraggedText = self.doc:get_text(self.doc:get_selection()) - self.dragged_selection = { iSelLine1, iSelCol1, iSelLine2, iSelCol2 } - else - self.bClickedIntoSelection = nil - self.dragged_selection = nil - -- let 'old' on_mouse_pressed() do whatever it needs to do - on_mouse_pressed(self, button, x, y, clicks) - end + local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks) + if caught then + return caught + end + -- no need to proceed if not left button or has no selection + if + ('left' ~= button) + or (not self.doc:has_selection()) + or (1 < clicks) + then + return on_mouse_pressed(self, button, x, y, clicks) + end + -- convert pixel coordinates to line and column coordinates + local iLine, iCol = self:resolve_screen_position(x, y) + -- get selection coordinates + local iSelLine1, iSelCol1, iSelLine2, iSelCol2 = self.doc:get_selection(true) + -- set flag for on_mouse_released and on_mouse_moved() methods to detect dragging + self.bClickedIntoSelection = isInSelection(iLine, iCol, iSelLine1, iSelCol1, + iSelLine2, iSelCol2) + if self.bClickedIntoSelection then + self.drag_start_loc = { x, y } + -- stash selection for inserting later + self.sDraggedText = self.doc:get_text(self.doc:get_selection()) + self.dragged_selection = { iSelLine1, iSelCol1, iSelLine2, iSelCol2 } + else + self.bClickedIntoSelection = nil + self.dragged_selection = nil + -- let 'old' on_mouse_pressed() do whatever it needs to do + on_mouse_pressed(self, button, x, y, clicks) + end end -- DocView:on_mouse_pressed -- override DocView:on_mouse_released() local on_mouse_released = DocView.on_mouse_released function DocView:on_mouse_released(button, x, y) - local iLine, iCol = self:resolve_screen_position(x, y) - if self.bClickedIntoSelection then - local iSelLine1, iSelCol1, iSelLine2, iSelCol2 = table.unpack(self.dragged_selection) - if not self.drag_start_loc - and not isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) then - -- insert stashed selected text at current position - if iLine < iSelLine1 or (iLine == iSelLine1 and iCol < iSelCol1) then - -- delete first - self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) - if not keymap.modkeys['ctrl'] then - self.doc:delete_to(0) - end - self.doc:set_selection(iLine, iCol) - self.doc:text_input(self.sDraggedText) - else - -- insert first - self.doc:set_selection(iLine, iCol) - self.doc:text_input(self.sDraggedText) - self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) - if not keymap.modkeys['ctrl'] then - self.doc:delete_to(0) - end - self.doc:set_selection(iLine, iCol) - end - elseif self.drag_start_loc then - -- deselect only if the drag never happened - self.doc:set_selection(iLine, iCol) + local iLine, iCol = self:resolve_screen_position(x, y) + if self.bClickedIntoSelection then + local iSelLine1, iSelCol1, iSelLine2, iSelCol2 = table.unpack(self.dragged_selection) + if + not self.drag_start_loc + and + not isInSelection(iLine, iCol, iSelLine1, iSelCol1, iSelLine2, iSelCol2) + then + -- insert stashed selected text at current position + if iLine < iSelLine1 or (iLine == iSelLine1 and iCol < iSelCol1) then + -- delete first + self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) + if not keymap.modkeys['ctrl'] then + self.doc:delete_to(0) end - -- unset stash and flag(s) TODO: - self.sDraggedText = '' - self.bClickedIntoSelection = nil + self.doc:set_selection(iLine, iCol) + self.doc:text_input(self.sDraggedText) + else + -- insert first + self.doc:set_selection(iLine, iCol) + self.doc:text_input(self.sDraggedText) + self.doc:set_selection(iSelLine1, iSelCol1, iSelLine2, iSelCol2) + if not keymap.modkeys['ctrl'] then + self.doc:delete_to(0) + end + self.doc:set_selection(iLine, iCol) + end + elseif self.drag_start_loc then + -- deselect only if the drag never happened + self.doc:set_selection(iLine, iCol) end + -- unset stash and flag(s) TODO: + self.sDraggedText = '' + self.bClickedIntoSelection = nil + end - -- hand over to old handler - on_mouse_released(self, button, x, y) + -- hand over to old handler + on_mouse_released(self, button, x, y) end -- DocView:on_mouse_released -- override DocView:draw_caret() local draw_caret = DocView.draw_caret function DocView:draw_caret(x, y) - if self.bClickedIntoSelection then - local iLine, iCol = self:resolve_screen_position(x, y) - -- don't show carets inside selections - if isInSelection(iLine, iCol, - self.dragged_selection[1], self.dragged_selection[2], - self.dragged_selection[3], self.dragged_selection[4]) then - return - end + if self.bClickedIntoSelection then + local iLine, iCol = self:resolve_screen_position(x, y) + -- don't show carets inside selections + if + isInSelection( + iLine, iCol, + self.dragged_selection[1], self.dragged_selection[2], + self.dragged_selection[3], self.dragged_selection[4] + ) + then + return end - draw_caret(self, x, y) + end + draw_caret(self, x, y) end -- DocView:draw_caret() diff --git a/plugins/ephemeral_tabs.lua b/plugins/ephemeral_tabs.lua index dea4261..f61c493 100644 --- a/plugins/ephemeral_tabs.lua +++ b/plugins/ephemeral_tabs.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local RootView = require "core.rootview" diff --git a/plugins/eval.lua b/plugins/eval.lua index bd1ff56..c2eb19e 100644 --- a/plugins/eval.lua +++ b/plugins/eval.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" @@ -13,12 +13,20 @@ end command.add("core.docview", { ["eval:insert"] = function() - core.command_view:enter("Evaluate And Insert Result", function(cmd) - core.active_view.doc:text_input(eval(cmd)) - end) + core.command_view:enter("Evaluate And Insert Result", { + submit = function(cmd) + core.active_view.doc:text_input(eval(cmd)) + end + }) end, ["eval:replace"] = function() - core.active_view.doc:replace(eval) + core.command_view:enter("Evaluate And Replace With Result", { + submit = function(cmd) + core.active_view.doc:replace(function(str) + return eval(cmd) + end) + end + }) end, }) diff --git a/plugins/exec.lua b/plugins/exec.lua index cf2d8f2..b8f61c5 100644 --- a/plugins/exec.lua +++ b/plugins/exec.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" @@ -28,19 +28,23 @@ end command.add("core.docview", { ["exec:insert"] = function() - core.command_view:enter("Insert Result Of Command", function(cmd) - core.active_view.doc:text_input(exec(cmd)) - end) + core.command_view:enter("Insert Result Of Command", { + submit = function(cmd) + core.active_view.doc:text_input(exec(cmd)) + end + }) end, ["exec:replace"] = function() - core.command_view:enter("Replace With Result Of Command", function(cmd) - core.active_view.doc:replace(function(str) - return exec( - "printf %b " .. printfb_quote(str:gsub("%\n$", "") .. "\n") .. " | eval '' " .. shell_quote(cmd), - str:find("%\n$") - ) - end) - end) + core.command_view:enter("Replace With Result Of Command", { + submit = function(cmd) + core.active_view.doc:replace(function(str) + return exec( + "printf %b " .. printfb_quote(str:gsub("%\n$", "") .. "\n") .. " | eval '' " .. shell_quote(cmd), + str:find("%\n$") + ) + end) + end + }) end, }) diff --git a/plugins/extend_selection_line.lua b/plugins/extend_selection_line.lua index e986597..e002674 100644 --- a/plugins/extend_selection_line.lua +++ b/plugins/extend_selection_line.lua @@ -1,19 +1,20 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local DocView = require "core.docview" local style = require "core.style" local draw_line_body = DocView.draw_line_body -function DocView:draw_line_body(idx, x, y, ...) - draw_line_body(self, idx, x, y, ...) +function DocView:draw_line_body(line, x, y) + local line_height = draw_line_body(self, line, x, y) local lh = self:get_line_height() for _, line1, _, line2, _ in self.doc:get_selections(true) do - if idx >= line1 and idx < line2 and line1 ~= line2 then + if line >= line1 and line < line2 and line1 ~= line2 then -- draw selection from the end of the line to the end of the available space - local x1 = x + self:get_col_x_offset(idx, #self.doc.lines[idx]) + local x1 = x + self:get_col_x_offset(line, #self.doc.lines[line]) local x2 = x + self.scroll.x + self.size.x if x2 > x1 then renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) end end end + return line_height end diff --git a/plugins/fontconfig.lua b/plugins/fontconfig.lua index 657e364..8b713b5 100644 --- a/plugins/fontconfig.lua +++ b/plugins/fontconfig.lua @@ -1,11 +1,12 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local subprocess = require "process" local core = require "core" local style = require "core.style" local config = require "core.config" +local common = require "core.common" -config.plugins.fontconfig = { prefix = "" } +config.plugins.fontconfig = common.merge({ prefix = "" }, config.plugins.fontconfig) --[[ Example config (put it in user module): diff --git a/plugins/force_syntax.lua b/plugins/force_syntax.lua index dce4abc..ae5a138 100644 --- a/plugins/force_syntax.lua +++ b/plugins/force_syntax.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local Doc = require "core.doc" local syntax = require "core.syntax" @@ -39,30 +39,24 @@ local function get_syntax_name(s) return name or "Undefined" end -local statusview_get_items = StatusView.get_items -function StatusView:get_items() - local left, right = statusview_get_items(self) - - local is_dv = core.active_view and getmetatable(core.active_view) == DocView - if not is_dv then return left, right end - - local syntax_name = get_syntax_name(doc().syntax) - - local ins = { - style.dim, - self.separator2, - style.text, - syntax_name - } - - if syntax_name then - for _,item in pairs(ins) do - table.insert(right, item) - end - end - - return left, right -end +core.status_view:add_item({ + predicate = function() + return core.active_view and getmetatable(core.active_view) == DocView + end, + name = "doc:syntax", + alignment = StatusView.Item.RIGHT, + get_item = function() + local syntax_name = get_syntax_name(doc().syntax) + return { + style.text, + syntax_name + } + end, + command = "force-syntax:select-file-syntax", + position = -1, + tooltip = "file syntax", + separator = core.status_view.separator2 +}) local function get_syntax_list() local pt_name = plain_text_syntax.name @@ -110,23 +104,20 @@ end command.add("core.docview", { ["force-syntax:select-file-syntax"] = function() - core.command_view:enter( - "Set syntax for this file", - function(text, item) -- submit + core.command_view:enter("Set syntax for this file", { + submit = function(text, item) local list, _ = get_syntax_list() doc().force_syntax = list[item.text] doc():reset_syntax() end, - function(text) -- suggest + suggest = function(text) local _, keylist = get_syntax_list() local res = common.fuzzy_match(keylist, text) -- Force Current and Auto detect syntax to the bottom -- if the text is empty table.sort(res, #text == 0 and bias_sorter or sorter) return res - end, - nil, -- cancel - nil -- validate - ) + end + }) end }) diff --git a/plugins/ghmarkdown.lua b/plugins/ghmarkdown.lua index 532be57..244b144 100644 --- a/plugins/ghmarkdown.lua +++ b/plugins/ghmarkdown.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/plugins/gitstatus.lua b/plugins/gitstatus.lua index de5c74b..9a36142 100644 --- a/plugins/gitstatus.lua +++ b/plugins/gitstatus.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" local config = require "core.config" @@ -6,15 +6,37 @@ local style = require "core.style" local StatusView = require "core.statusview" local TreeView = require "plugins.treeview" +config.plugins.gitstatus = common.merge({ + recurse_submodules = true, + -- The config specification used by the settings gui + config_spec = { + name = "Git Status", + { + label = "Recurse Submodules", + description = "Also retrieve git stats from submodules.", + path = "recurse_submodules", + type = "toggle", + default = true + } + } +}, config.plugins.gitstatus) + +style.gitstatus_addition = {common.color "#587c0c"} +style.gitstatus_modification = {common.color "#0c7d9d"} +style.gitstatus_deletion = {common.color "#94151b"} + local scan_rate = config.project_scan_rate or 5 local cached_color_for_item = {} --- Override TreeView's color_for_item, but first --- stash the old one (using [] in case it is not there at all) -local old_color_for_item = TreeView["color_for_item"] -function TreeView:color_for_item(abs_path) - return cached_color_for_item[abs_path] or old_color_for_item(abs_path) +-- Override TreeView's get_item_text to add modification color +local treeview_get_item_text = TreeView.get_item_text +function TreeView:get_item_text(item, active, hovered) + local text, font, color = treeview_get_item_text(self, item, active, hovered) + if cached_color_for_item[item.abs_filename] then + color = cached_color_for_item[item.abs_filename] + end + return text, font, color end @@ -24,15 +46,6 @@ local git = { deletes = 0, } - -config.gitstatus = { - recurse_submodules = true -} -style.gitstatus_addition = {common.color "#587c0c"} -style.gitstatus_modification = {common.color "#0c7d9d"} -style.gitstatus_deletion = {common.color "#94151b"} - - local function exec(cmd) local proc = process.start(cmd) -- Don't use proc:wait() here - that will freeze the app. @@ -57,7 +70,11 @@ core.add_thread(function() -- get diff local diff = exec({"git", "diff", "--numstat"}) - if config.gitstatus.recurse_submodules and system.get_file_info(".gitmodules") then + if + config.plugins.gitstatus.recurse_submodules + and + system.get_file_info(".gitmodules") + then local diff2 = exec({"git", "submodule", "foreach", "git diff --numstat"}) diff = diff .. diff2 end @@ -99,27 +116,23 @@ core.add_thread(function() end) -local get_items = StatusView.get_items - -function StatusView:get_items() - if not git.branch then - return get_items(self) - end - local left, right = get_items(self) - - local t = { - style.dim, self.separator, - (git.inserts ~= 0 or git.deletes ~= 0) and style.accent or style.text, - git.branch, - style.dim, " ", - git.inserts ~= 0 and style.accent or style.text, "+", git.inserts, - style.dim, " / ", - git.deletes ~= 0 and style.accent or style.text, "-", git.deletes, - } - for _, item in ipairs(t) do - table.insert(right, item) - end - - return left, right -end - +core.status_view:add_item({ + name = "status:git", + alignment = StatusView.Item.RIGHT, + get_item = function() + if not git.branch then + return {} + end + return { + (git.inserts ~= 0 or git.deletes ~= 0) and style.accent or style.text, + git.branch, + style.dim, " ", + git.inserts ~= 0 and style.accent or style.text, "+", git.inserts, + style.dim, " / ", + git.deletes ~= 0 and style.accent or style.text, "-", git.deletes, + } + end, + position = -1, + tooltip = "branch and changes", + separator = core.status_view.separator2 +}) diff --git a/plugins/gofmt.lua b/plugins/gofmt.lua index 02c817b..fec95e4 100644 --- a/plugins/gofmt.lua +++ b/plugins/gofmt.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/plugins/hidelinenumbers.lua b/plugins/hidelinenumbers.lua deleted file mode 100644 index 4494670..0000000 --- a/plugins/hidelinenumbers.lua +++ /dev/null @@ -1,6 +0,0 @@ --- mod-version:2 -- lite-xl 2.0 -local style = require "core.style" -local DocView = require "core.docview" - -DocView.draw_line_gutter = function() end -DocView.get_gutter_width = function() return style.padding.x end diff --git a/plugins/hidestatus.lua b/plugins/hidestatus.lua deleted file mode 100644 index 6e63a81..0000000 --- a/plugins/hidestatus.lua +++ /dev/null @@ -1,19 +0,0 @@ --- mod-version:2 -- lite-xl 2.0 -local command = require "core.command" -local StatusView = require "core.statusview" - -local visible = false -local funcs = { - [true] = StatusView.update, - [false] = function(self) self.size.y = 0 end, -} - -function StatusView:update(...) - funcs[visible](self, ...) -end - -command.add(nil, { - ["hide-status:toggle"] = function() visible = not visible end, - ["hide-status:hide"] = function() visible = false end, - ["hide-status:show"] = function() visible = true end, -}) diff --git a/plugins/indent_convert.lua b/plugins/indent_convert.lua index c86686d..3a950f5 100644 --- a/plugins/indent_convert.lua +++ b/plugins/indent_convert.lua @@ -1,11 +1,24 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" +local common = require "core.common" local config = require "core.config" local command = require "core.command" -config.plugins.indent_convert = { - update_indent_type = true -- set to false to avoid updating the document indent type -} +config.plugins.indent_convert = common.merge({ + -- set to false to avoid updating the document indent type + update_indent_type = true, + -- The config specification used by the settings gui + config_spec = { + name = "Indent Convert", + { + label = "Update Indent Type", + description = "Disable to avoid updating the document indent type.", + path = "update_indent_type", + type = "toggle", + default = true + } + } +}, config.plugins.indent_convert) local zero_pattern = _VERSION == "Lua 5.1" and "%z" or "\0" diff --git a/plugins/indentguide.lua b/plugins/indentguide.lua index 99b1311..42eb3a6 100644 --- a/plugins/indentguide.lua +++ b/plugins/indentguide.lua @@ -1,8 +1,23 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local style = require "core.style" local config = require "core.config" +local common = require "core.common" local DocView = require "core.docview" +config.plugins.indentguide = common.merge({ + enabled = true, + -- The config specification used by the settings gui + config_spec = { + name = "Indent Guide", + { + label = "Enable", + description = "Toggle the drawing of indentation indicator lines.", + path = "enabled", + type = "toggle", + default = true + } + } +}, config.plugins.indentguide) -- TODO: replace with `doc:get_indent_info()` when 2.1 releases local function get_indent_info(doc) @@ -13,15 +28,15 @@ local function get_indent_info(doc) end -local function get_line_spaces(doc, idx, dir) +local function get_line_spaces(doc, line, dir) local _, indent_size = get_indent_info(doc) - local text = doc.lines[idx] + local text = doc.lines[line] if not text or #text == 1 then return -1 end local s, e = text:find("^%s*") if e == #text then - return get_line_spaces(doc, idx + dir, dir) + return get_line_spaces(doc, line + dir, dir) end local n = 0 for _,b in pairs({text:byte(s, e)}) do @@ -31,25 +46,29 @@ local function get_line_spaces(doc, idx, dir) end -local function get_line_indent_guide_spaces(doc, idx) - if doc.lines[idx]:find("^%s*\n") then +local function get_line_indent_guide_spaces(doc, line) + if doc.lines[line]:find("^%s*\n") then return math.max( - get_line_spaces(doc, idx - 1, -1), - get_line_spaces(doc, idx + 1, 1)) + get_line_spaces(doc, line - 1, -1), + get_line_spaces(doc, line + 1, 1)) end - return get_line_spaces(doc, idx) + return get_line_spaces(doc, line) end local docview_update = DocView.update function DocView:update() docview_update(self) - local function get_indent(idx) - if idx < 1 or idx > #self.doc.lines then return -1 end - if not self.indentguide_indents[idx] then - self.indentguide_indents[idx] = get_line_indent_guide_spaces(self.doc, idx) + if not config.plugins.indentguide.enabled or not self:is(DocView) then + return + end + + local function get_indent(line) + if line < 1 or line > #self.doc.lines then return -1 end + if not self.indentguide_indents[line] then + self.indentguide_indents[line] = get_line_indent_guide_spaces(self.doc, line) end - return self.indentguide_indents[idx] + return self.indentguide_indents[line] end self.indentguide_indents = {} @@ -103,21 +122,23 @@ end local draw_line_text = DocView.draw_line_text -function DocView:draw_line_text(idx, x, y) - local spaces = self.indentguide_indents[idx] or -1 - local _, indent_size = get_indent_info(self.doc) - local w = math.max(1, SCALE) - local h = self:get_line_height() - local font = self:get_font() - local space_sz = font:get_width(" ") - for i = 0, spaces - 1, indent_size do - local color = style.guide or style.selection - local active_lvl = self.indentguide_indent_active[idx] or -1 - if i < active_lvl and i + indent_size >= active_lvl then - color = style.guide_highlight or style.accent +function DocView:draw_line_text(line, x, y) + if config.plugins.indentguide.enabled and self:is(DocView) then + local spaces = self.indentguide_indents[line] or -1 + local _, indent_size = get_indent_info(self.doc) + local w = math.max(1, SCALE) + local h = self:get_line_height() + local font = self:get_font() + local space_sz = font:get_width(" ") + for i = 0, spaces - 1, indent_size do + local color = style.guide or style.selection + local active_lvl = self.indentguide_indent_active[line] or -1 + if i < active_lvl and i + indent_size >= active_lvl then + color = style.guide_highlight or style.accent + end + local sw = space_sz * i + renderer.draw_rect(math.ceil(x + sw), y, w, h, color) end - local sw = space_sz * i - renderer.draw_rect(math.ceil(x + sw), y, w, h, color) end - draw_line_text(self, idx, x, y) + return draw_line_text(self, line, x, y) end diff --git a/plugins/ipc.lua b/plugins/ipc.lua new file mode 100644 index 0000000..8a68fc2 --- /dev/null +++ b/plugins/ipc.lua @@ -0,0 +1,1043 @@ +-- mod-version:3 +-- +-- Crossplatform file based IPC system for lite-xl. +-- @copyright Jefferson Gonzalez <jgmdev@gmail.com> +-- @license MIT +-- +local core = require "core" +local config = require "core.config" +local common = require "core.common" +local command = require "core.command" +local Object = require "core.object" +local RootView = require "core.rootview" +local settings_found, settings = pcall(require, "plugins.settings") + +---The maximum amount of seconds a message will be broadcasted. +---@type integer +local MESSAGE_EXPIRATION=3 + +---@class config.plugins.ipc +---@field single_instance boolean +---@field dirs_instance string +config.plugins.ipc = common.merge({ + single_instance = true, + dirs_instance = "new", + -- The config specification used by the settings gui + config_spec = { + name = "Inter-process communication", + { + label = "Single Instance", + description = "Run a single instance of lite-xl.", + path = "single_instance", + type = "toggle", + default = true + }, + { + label = "Directories Instance", + description = "Control how to open directories in single instance mode.", + path = "dirs_instance", + type = "selection", + default = "new", + values = { + {"Create a New Instance", "new"}, + {"Add to Current Instance", "add"}, + {"Change Current Instance Project Directory", "change"} + } + } + } +}, config.plugins.ipc) + +---@alias plugins.ipc.onmessageread fun(message: plugins.ipc.message) | nil +---@alias plugins.ipc.onreplyread fun(reply: plugins.ipc.reply) | nil +---@alias plugins.ipc.onmessage fun(message: plugins.ipc.message, reply: plugins.ipc.reply) | nil +---@alias plugins.ipc.onreply fun(reply: plugins.ipc.reply) | nil +---@alias plugins.ipc.function fun(...) + +---@alias plugins.ipc.messagetype +---| '"message"' +---| '"method"' +---| '"signal"' + +---@class plugins.ipc.message +---@field id string +---@field sender string +---@field name string +---@field type plugins.ipc.messagetype | string +---@field destinations table<integer,string> +---@field data table<string,any> +---@field timestamp number +---@field on_read plugins.ipc.onmessageread +---@field on_reply plugins.ipc.onreply +---@field replies plugins.ipc.reply[] +local IPCMessage = { + ---Id of the message + id = "", + ---The id of process that sent the message + sender = "", + ---Name of the message + name = "", + ---Type of message. + type = "", + ---List with id of the instance that should receive the message. + destinations = {}, + ---A list of named values sent to receivers. + data = {}, + ---Time in seconds when the message was sent for automatic expiration purposes. + timestamp = 0, + ---Optional callback executed by the receiver when the message is read. + on_read = function(message) end, + ---Optional callback executed when a reply to the message is received. + on_reply = function(reply) end, + ---The received replies for the message. + replies = {} +} + +---@class plugins.ipc.reply +---@field id string +---@field sender string +---@field replier string +---@field data table<string,any> +---@field timestamp number +---@field on_read plugins.ipc.onreplyread +local IPCReply = { + ---Id of the message + id = "", + ---The id of process that sent the message + sender = "", + ---The id of the replier + replier = "", + ---A list of named values sent back to sender. + data = {}, + ---Time in seconds when the reply was sent for automatic expiration purposes. + timestamp = 0, + ---Optional callback executed by the sender when the reply is read. + on_read = function(reply) end +} + +---@class plugins.ipc.instance +---@field id string +---@field position integer +---@field last_update integer +---@field messages plugins.ipc.message[] +---@field replies plugins.ipc.reply[] +---@field properties table +local IPCInstance = { + ---Process id of the instance. + id = "", + ---The position in which the instance was launched. + position = 0, + ---Flag that indicates if this instance was the first started. + primary = false, + ---Indicates the last time this instance updated its session file. + last_update = 0, + ---The messages been broadcasted. + messages = {}, + ---The replies been broadcasted. + replies = {}, + ---Table of properties associated with the instance. + properties = {} +} + +---@class core.ipc : core.object +---@field private id string +---@field private user_dir string +---@field private running boolean +---@field private file string +---@field private primary boolean +---@field private position integer +---@field private messages plugins.ipc.message[] +---@field private replies plugins.ipc.reply[] +---@field private listeners table<string,table<integer,plugins.ipc.onmessage>> +---@field private signals table<string,integer> +---@field private methods table<string,integer> +---@field private signal_definitions table<integer,string> +---@field private method_definitions table<integer,string> +local IPC = Object:extend() + +---@class plugins.ipc.threads +---@field cr thread +---@field wake number + +---List of threads belonging to all instantiated IPC objects. +---@type plugins.ipc.threads[] +local threads = {} + +---Register a new thread to be run on the background. +---@param f function +local function add_thread(f) + local key = #threads + 1 + threads[key] = { cr = coroutine.create(f), wake = 0 } + return key +end + +---Updates the session file of an IPC object. +---@param self core.ipc +local function update_file(self) + local file, errmsg = io.open(self.file, "w+") + + if file then + local output = "-- Warning: Generated by IPC system do not edit manually!\n" + .. "return " .. common.serialize( + { + id = self.id, + primary = self.primary, + position = self.position, + last_update = os.time(), + messages = self.messages, + replies = self.replies, + signals = self.signal_definitions, + methods = self.method_definitions + }, + { + pretty = true + } + ) + + output = output:gsub("%s+%[\"on_reply\"%].-\n", "") + + file:write(output) + file:close() + else + core.error("IPC Error: failed updating status (%s)", errmsg) + end +end + +---Constructor +---@param id? string Defaults to current lite-xl process id. +function IPC:new(id) + self.id = id or tostring(system.get_process_id()) + self.user_dir = USERDIR .. "/ipc" + self.file = self.user_dir .. "/" .. self.id .. ".lua" + self.primary = false + self.running = false + self.messages = {} + self.replies = {} + self.listeners = {} + self.signals = {} + self.methods = {} + self.signal_definitions = {} + self.method_definitions = {} + + local ipc_dir_status = system.get_file_info(self.user_dir) + + if not ipc_dir_status then + local created, errmsg = common.mkdirp(self.user_dir) + if not created then + core.error("Error initializing IPC system: %s", errmsg) + return + end + end + + local file, errmsg = io.open(self.file, "w+") + + if not file then + core.error("Error initializing IPC system: %s", errmsg) + return + else + file:close() + os.remove(self.file) + end + + -- Execute to set the instance position and primary attribute if no other running. + local instances = self:get_instances() + self.primary = #instances == 0 and true or false + self.position = #instances + 1 + + self:start() +end + +---Starts and registers the ipc session and monitoring. +function IPC:start() + if not self.running then + self.running = true + + update_file(self) + + local wait_time = 0.3 + + local this = self + self.coroutine_key = add_thread(function() + coroutine.yield(wait_time) + while(true) do + this:read_messages() + this:read_replies() + update_file(this) + coroutine.yield(wait_time) + end + end) + end +end + +---Stop and unregister the ipc session and monitoring. +function IPC:stop() + self.running = false + table.remove(threads, self.coroutine_key) + os.remove(self.file) +end + +---Get a list of running lite-xl instances. +---@return plugins.ipc.instance[] +function IPC:get_instances() + ---@type plugins.ipc.instance[] + local instances = {} + + local files, errmsg = system.list_dir(self.user_dir) + + if files then + for _, file in ipairs(files) do + if string.match(file, "^%d+%.lua$") then + local path = self.user_dir .. "/" .. file + local file_info = system.get_file_info(path) + if file_info and file_info.type == "file" then + ::read_instance_file:: + ---@type plugins.ipc.instance + local instance = dofile(path) + if instance and instance.id ~= self.id then + if instance.last_update + 2 > os.time() then + table.insert(instances, instance) + else + -- Delete expired instance session maybe result of a crash + os.remove(path) + end + elseif not instance and path ~= self.file then + --We retry reading the file since it was been modified + --by its owner instance. + goto read_instance_file + end + end + end + end + else + core.error("IPC Error: failed getting running instances (%s)", errmsg) + end + + local instances_count = #instances + + if instances_count > 0 then + table.sort(instances, function(ia, ib) + return ia.position < ib.position + end) + end + + if not self.primary and self.position then + if instances_count == 0 or instances[1].position > self.position then + self.primary = true + end + end + + return instances +end + +---@class plugins.ipc.vardecl +---@field name string +---@field type string +---@field optional boolean + +---Generate a string representation of a function +---@param name string +---@param params? plugins.ipc.vardecl[] +---@param returns? plugins.ipc.vardecl[] +---@return string function_definition +local function generate_definition(name, params, returns) + local declaration = name .. "(" + + if params and #params > 0 then + local params_string = "" + for _, param in ipairs(params) do + params_string = params_string .. param.name + if param.optional then + params_string = params_string .. "?: " + else + params_string = params_string .. ": " + end + params_string = params_string .. param.type .. ", " + end + local params_stripped = params_string:gsub(", $", "") + declaration = declaration .. params_stripped + end + + declaration = declaration .. ")" + + if returns and #returns > 0 then + declaration = declaration .. " -> " + local returns_string = "" + for _, ret in ipairs(returns) do + if ret.name then + returns_string = returns_string .. ret.name .. ": " + end + returns_string = returns_string .. ret.type + if ret.optional then + returns_string = returns_string .. "?, " + else + returns_string = returns_string .. ", " + end + end + local returns_stripped = returns_string:gsub(", $", "") + declaration = declaration .. returns_stripped + end + + return declaration +end + +---Retrieve the id of the primary instance if found. +---@return string | nil +function IPC:get_primary_instance() + local instances = self:get_instances() + for _, instance in ipairs(instances) do + if instance.primary then + return instance.id + end + end + return nil +end + +---Get a queued message. +---@param message_id string +---@return plugins.ipc.message | nil +function IPC:get_message(message_id) + for _, message in ipairs(self.messages) do + if message.id == message_id then + return message + end + end + return nil +end + +---Remove a message from the queue. +---@param message_id string +function IPC:remove_message(message_id) + for m, message in ipairs(self.messages) do + if message.id == message_id then + table.remove(self.messages, m) + break + end + end +end + +---Get the reply sent to a specific message. +---@param message_id string +---@return plugins.ipc.reply | nil +function IPC:get_reply(message_id) + for _, reply in ipairs(self.replies) do + if reply.id == message_id then + return reply + end + end + return nil +end + +---Verify all the messages sent by running instances, read those directed +---to the currently running instance and reply to them. +function IPC:read_messages() + local instances = self:get_instances() + + local awaiting_replies = {} + + for _, instance in ipairs(instances) do + for _, message in ipairs(instance.messages) do + for _, destination in ipairs(message.destinations) do + if destination == self.id then + local reply = self:get_reply(message.id) + + if not reply then + if message.on_read then + local on_read, errmsg = load(message.on_read) + if on_read then + local executed = core.try(function() on_read(message) end) + if not executed then + core.error( + "IPC Error: could not run message on_read\n" + .. "Message: %s\n", + common.serialize(message, {pretty = true}) + ) + end + else + core.error( + "IPC Error: could not run message on_read (%s)\n" + .. "Message: %s\n", + errmsg, + common.serialize(message, {pretty = true}) + ) + end + end + + ---@type plugins.ipc.reply + reply = {} + reply.id = message.id + reply.sender = message.sender + reply.replier = self.id + reply.data = {} + reply.on_read = nil + + local type_name = message.type .. "." .. message.name + + -- Allow listeners to react to message and modify reply + if self.listeners[type_name] and #self.listeners[type_name] > 0 then + for _, on_message in ipairs(self.listeners[type_name]) do + on_message(message, reply) + end + end + + if reply.on_read then + reply.on_read = string.dump(reply.on_read) + end + + reply.timestamp = os.time() + end + + table.insert(awaiting_replies, reply) + break + end + end + end + end + + self.replies = awaiting_replies +end + +---Reads replies directed to messages sent by the currently running instance +---and if any returns them. +---@return plugins.ipc.reply[] | nil +function IPC:read_replies() + if #self.messages == 0 then + return + end + + local instances = self:get_instances() + + local replies = {} + + local messages_removed = 0; + for m=1, #self.messages do + local message = self.messages[m-messages_removed] + local message_removed = false + + local destinations_removed = 0 + for d=1, #message.destinations do + local destination = message.destinations[d-destinations_removed] + + local found = false + for _, instance in ipairs(instances) do + if instance.id == destination then + found = true + for _, reply in ipairs(instance.replies) do + if reply.id == message.id then + local reply_registered = false + for _, message_reply in ipairs(message.replies) do + if message_reply.replier == instance.id then + reply_registered = true + break + end + end + if not reply_registered then + if message.on_reply then + message.on_reply(reply) + end + + if reply.on_read then + local on_read, errmsg = load(reply.on_read) + if on_read then + local executed = core.try(function() on_read(reply) end) + if not executed then + core.error( + "IPC Error: could not run reply on_read\n" + .. "Message: %s\n" + .. "Reply: %s", + common.serialize(message, {pretty = true}), + common.serialize(reply, {pretty = true}) + ) + end + else + core.error( + "IPC Error: could not run reply on_read (%s)\n" + .. "Message: %s\n" + .. "Reply: %s", + errmsg, + common.serialize(message, {pretty = true}), + common.serialize(reply, {pretty = true}) + ) + end + end + + table.insert(replies, reply) + table.insert(message.replies, reply) + end + end + end + break + end + end + if not found then + table.remove(message.destinations, d-destinations_removed) + destinations_removed = destinations_removed + 1 + if #message.destinations == 0 then + table.remove(self.messages, m-messages_removed) + messages_removed = messages_removed + 1 + message_removed = true + end + end + end + if + not message_removed + and + ( + #message.replies == #message.destinations + or + message.timestamp + MESSAGE_EXPIRATION < os.time() + ) + then + table.remove(self.messages, m-messages_removed) + messages_removed = messages_removed + 1 + end + end + + return replies +end + +---Blocks execution of current instance to wait for all replies by the +---specified message and when finished returns them. +---@param message_id string +---@return plugins.ipc.reply[] | nil +function IPC:wait_for_replies(message_id) + local message_data = self:get_message(message_id) + + update_file(self) + + if message_data then + self:read_replies() + while true do + if + message_data.replies + and + #message_data.replies == #message_data.destinations + then + return message_data.replies + elseif not self:get_message(message_id) then + return message_data.replies + end + self:read_replies() + end + end + return nil +end + +---Blocks execution of current instance to wait for all messages to +---be replied to. +function IPC:wait_for_messages() + update_file(self) + while #self.messages > 0 do + self:read_replies() + system.sleep(0.1) + end +end + +---@class plugins.ipc.sendmessageoptions +---@field data table<string,any> @Optional data given to the receiver. +---@field on_reply plugins.ipc.onreply @Callback that allows monitoring all the replies received for this message. +---@field on_read plugins.ipc.onmessage @Function executed by the message receiver. +---@field destinations string | table<integer,string> | nil @Id of the running instances to receive the message, if not set all running instances will receive the message. + +---Queue a new message to be sent to other lite-xl instances. +---@param name string +---@param options? plugins.ipc.sendmessageoptions +---@param message_type? plugins.ipc.messagetype +---@return string | nil message_id +function IPC:send_message(name, options, message_type) + options = options or {} + + local found_destinations = {} + local instances = self:get_instances() + local destinations = options.destinations + + if type(destinations) == "string" then + destinations = { destinations } + end + + if not destinations then + for _, instance in ipairs(instances) do + table.insert(found_destinations, instance.id) + end + else + for _, destination in ipairs(destinations) do + for _, instance in ipairs(instances) do + if instance.id == destination then + table.insert(found_destinations, destination) + end + end + end + end + + if #found_destinations <= 0 then + return nil + end + + ---@type plugins.ipc.message + local message = {} + message.id = self.id .. "." .. tostring(system.get_time()) + message.name = name + message.type = message_type or "message" + message.sender = self.id + message.data = options.data or {} + message.destinations = found_destinations + message.timestamp = os.time() + message.on_reply = options.on_reply or nil + message.on_read = options.on_read and string.dump(options.on_read) or nil + message.replies = {} + + table.insert(self.messages, message) + + update_file(self) + + return message.id +end + +---Add a listener for a given type of message. +---@param name string +---@param callback plugins.ipc.onmessage +---@param message_type? plugins.ipc.messagetype +---@return integer listener_position +function IPC:listen_message(name, callback, message_type) + message_type = message_type or "message" + + local type_name = message_type .. "." .. name + if not self.listeners[type_name] then + self.listeners[type_name] = {} + end + + table.insert(self.listeners[type_name], callback) + + return #self.listeners[type_name] +end + +---Listen for a given signal. +---@param name string +---@param callback plugins.ipc.function +---@return integer listener_position +function IPC:listen_signal(name, callback) + local signal_cb = function(message) + callback(table.unpack(message.data)) + end + return self:listen_message(name, signal_cb, "signal") +end + +---Add a new signal that can be sent to other instances. +---@param name string A unique name for the signal. +---@param params? plugins.ipc.vardecl[] Parameters that are going to be passed into callback. +function IPC:register_signal(name, params) + if self.signals[name] then + core.log_quiet("IPC: Overriding signal '%s'", name) + table.remove(self.signal_definitions, self.signals[name]) + end + + self.signals[name] = table.insert( + self.signal_definitions, + generate_definition(name, params) + ) + + table.sort(self.signal_definitions) +end + +---Add a new method that can be invoked from other instances. +---@param name string A unique name for the method. +---@param method fun(...) Function invoked when the method is called. +---@param params? plugins.ipc.vardecl[] Parameters that are going to be passed into method. +---@param returns? plugins.ipc.vardecl[] Return values of the method. +function IPC:register_method(name, method, params, returns) + if self.methods[name] then + core.log_quiet("IPC: Overriding method '%s'", name) + table.remove(self.method_definitions, self.methods[name]) + end + + self.methods[name] = table.insert( + self.method_definitions, + generate_definition(name, params, returns) + ) + + table.sort(self.method_definitions) + + self:listen_message(name, function(message, reply) + local ret = table.pack(method(table.unpack(message.data))) + reply.data = ret + end, "method") +end + +---Broadcast a signal to running instances. +---@param destinations string | table<integer, string> | nil +---@param name string +---@vararg any signal_parameters +function IPC:signal(destinations, name, ...) + self:send_message(name, { + destinations = destinations, + data = table.pack(self.id, ...) + }, "signal") +end + +---Call a method on another instance and wait for reply. +---@param destinations string | table<integer, string> | nil +---@param name string +---@return any | table<string,table> return_of_called_method +function IPC:call(destinations, name, ...) + local message_id = self:send_message(name, { + destinations = destinations, + data = table.pack(...) + }, "method") + + local ret = nil + + if message_id then + local replies = self:wait_for_replies(message_id) + if replies and #replies > 1 then + ret = {} + for _, reply in ipairs(replies) do + ret[reply.replier] = reply.data + end + elseif replies and #replies > 0 then + return table.unpack(replies[1].data) + end + else + core.error("IPC Error: could not make call to '%s'", name) + end + + return ret +end + +---Call a method on another instance asynchronously waiting for the replies. +---@param destinations string | table<integer, string> | nil +---@param name string +---@param callback fun(id: string, ret: table) | nil Called with the returned values +---@return string | nil message_id +function IPC:call_async(destinations, name, callback, ...) + return self:send_message(name, { + destinations = destinations, + data = table.pack(...), + on_reply = callback and function(reply) + callback(reply.replier, reply.data) + end or nil + }, "method") +end + +---Main ipc session for current instance. +---@type core.ipc +local ipc = IPC() + +---Get the IPC session for the running lite-xl instance. +---@return core.ipc +function IPC.current() + return ipc +end + +-------------------------------------------------------------------------------- +-- Override system.wait_event to allow ipc monitoring on the background. +-------------------------------------------------------------------------------- +local system_wait_event = system.wait_event + +local run_threads = coroutine.wrap(function() + while true do + for k, thread in pairs(threads) do + if thread.wake < system.get_time() then + local _, wait = assert(coroutine.resume(thread.cr)) + if coroutine.status(thread.cr) == "dead" then + table.remove(threads, k) + elseif wait then + thread.wake = system.get_time() + wait + end + end + coroutine.yield() + end + end +end) + +system.wait_event = function(timeout) + run_threads() + + if not timeout then + if not system.window_has_focus() then + local t = system.get_time() + local h = 0.5 / 2 + local dt = math.ceil(t / h) * h - t + + system_wait_event(dt + 1 / config.fps) + else + system_wait_event() + end + else + system_wait_event(timeout) + end +end + +-------------------------------------------------------------------------------- +-- Override system.show_fatal_error to be able and destroy session file on crash. +-------------------------------------------------------------------------------- +local system_show_fatal_error = system.show_fatal_error + +system.show_fatal_error = function(title, message) + if title == "Lite XL internal error" then + ipc:stop() + end + system_show_fatal_error(title, message) +end + +-------------------------------------------------------------------------------- +-- Override core.run to destroy ipc session file on exit. +-------------------------------------------------------------------------------- +local core_run = core.run + +core.run = function() + core_run() + ipc:stop() +end + +-------------------------------------------------------------------------------- +-- Override system.get_time temporarily as first function called on core.run +-- to allow settings gui to properly load ipc config options. +-------------------------------------------------------------------------------- +local system_get_time = system.get_time + +system.get_time = function() + if settings_found and not settings.ui then + return system_get_time() + end + + if config.plugins.ipc.single_instance then + system.get_time = system_get_time + + local primary_instance = ipc:get_primary_instance() + if primary_instance and ARGS[2] then + local open_directory = false + for i=2, #ARGS do + local path = system.absolute_path(ARGS[i]) + + if path then + local path_info = system.get_file_info(path) + if path_info then + if path_info.type == "file" then + ipc:call_async(primary_instance, "core.open_file", nil, path) + else + if config.plugins.ipc.dirs_instance == "add" then + ipc:call_async(primary_instance, "core.open_directory", nil, path) + elseif config.plugins.ipc.dirs_instance == "change" then + ipc:call_async(primary_instance, "core.change_directory", nil, path) + else + if #ARGS > 2 then + system.exec(string.format("%q %q", EXEFILE, path)) + else + open_directory = true + end + end + end + end + end + end + ipc:wait_for_messages() + if not open_directory then + os.exit() + end + end + else + system.get_time = system_get_time + end + + return system_get_time() +end + +-------------------------------------------------------------------------------- +-- Register methods for opening files and directories. +-------------------------------------------------------------------------------- +ipc:register_method("core.open_file", function(file) + if system.get_file_info(file) then + if system.raise_window then system.raise_window() end + core.root_view:open_doc(core.open_doc(file)) + end +end, {{name = "file", type = "string"}}) + +ipc:register_method("core.open_directory", function(directory) + if system.get_file_info(directory) then + if system.raise_window then system.raise_window() end + core.add_project_directory(directory) + end +end, {{name = "directory", type = "string"}}) + +ipc:register_method("core.change_directory", function(directory) + if system.get_file_info(directory) then + if system.raise_window then system.raise_window() end + if directory == core.project_dir then return end + core.confirm_close_docs(core.docs, function(dirpath) + core.open_folder_project(dirpath) + end, directory) + end +end, {{name = "directory", type = "string"}}) + +-------------------------------------------------------------------------------- +-- Register file dragging signals from instance to instance +-------------------------------------------------------------------------------- +ipc:register_signal("core.tab_drag_start", {{name = "file", type = "string"}}) +ipc:register_signal("core.tab_drag_stop") +ipc:register_signal("core.tab_drag_received", {{name = "file", type = "string"}}) + +local rootview_tab_dragging = false +local rootview_dragged_node = nil +local rootview_waiting_drop_file = "" +local rootview_waiting_drop_instance = "" + +local rootview_on_mouse_moved = RootView.on_mouse_moved +function RootView:on_mouse_moved(x, y, dx, dy) + rootview_on_mouse_moved(self, x, y, dx, dy) + if + self.dragged_node and self.dragged_node.dragging + and + not rootview_tab_dragging + then + ---@type core.doc + local doc = core.active_view.doc + if doc and doc.abs_filename then + rootview_tab_dragging = true + ipc:signal(nil, "core.tab_drag_start", doc.abs_filename) + rootview_dragged_node = self.dragged_node + end + elseif rootview_dragged_node then + local w, h, wx, wy = system.get_window_size() + if x < 0 or x > w or y < 0 or y > h then + self.dragged_node = nil + self:set_show_overlay(self.drag_overlay, false) + elseif not self.dragged_node then + self.dragged_node = rootview_dragged_node + self:set_show_overlay(self.drag_overlay, true) + end + core.request_cursor("hand") + elseif rootview_waiting_drop_file ~= "" then + ipc:signal( + rootview_waiting_drop_instance, + "core.tab_drag_received", + rootview_waiting_drop_file + ) + core.root_view:open_doc(core.open_doc(rootview_waiting_drop_file)) + rootview_waiting_drop_file = "" + end +end + +local rootview_on_mouse_released = RootView.on_mouse_released +function RootView:on_mouse_released(button, x, y, ...) + rootview_on_mouse_released(self, button, x, y, ...) + if rootview_tab_dragging then + rootview_tab_dragging = false + rootview_dragged_node = nil + ipc:signal(nil, "core.tab_drag_stop") + end +end + +ipc:listen_signal("core.tab_drag_start", function(instance, file) + rootview_waiting_drop_instance = instance + rootview_waiting_drop_file = file +end) + +ipc:listen_signal("core.tab_drag_stop", function() + rootview_waiting_drop_instance = "" + rootview_waiting_drop_file = "" +end) + +ipc:listen_signal("core.tab_drag_received", function() + command.perform("root:close") +end) + + +return IPC diff --git a/plugins/language_R.lua b/plugins/language_R.lua index ad3b483..afe3d1e 100644 --- a/plugins/language_R.lua +++ b/plugins/language_R.lua @@ -1,40 +1,39 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add{ - name = "R", - files = {"%.r$", "%.rds$", "%.rda$", "%.rdata$", "%.R$"}, - comment = "#", - patterns = { - {pattern = {"#", "\n"}, type = "comment"}, - {pattern = {'"', '"'}, type = "string"}, - {pattern = {"'", "'"}, type = "string"}, - {pattern = "[%a_][%w_]*%f[(]", type = "function"}, - {pattern = "[%a_][%w_]*", type = "symbol"}, - {pattern = "[%+%-=/%*%^%%<>!|&]", type = "operator"}, - {pattern = "0x[%da-fA-F]+", type = "number"}, - {pattern = "-?%d+[%d%.eE]*", type = "number"}, - {pattern = "-?%.?%d+", type = "number"}, - - }, - symbols = { - ["TRUE"] = "literal", - ["FALSE"] = "literal", - ["NA"] = "literal", - ["NULL"] = "literal", - ["Inf"] = "literal", - ["if"] = "keyword", - ["else"] = "keyword", - ["while"] = "keyword", - ["function"] = "keyword", - ["break"] = "keyword", - ["next"] = "keyword", - ["repeat"] = "keyword", - ["in"] = "keyword", - ["for"] = "keyword", - ["NA_integer"] = "keyword", - ["NA_complex"] = "keyword", - ["NA_character"] = "keyword", - ["NA_real"] = "keyword" - } + name = "R", + files = {"%.r$", "%.rds$", "%.rda$", "%.rdata$", "%.R$"}, + comment = "#", + patterns = { + {pattern = {"#", "\n"}, type = "comment"}, + {pattern = {'"', '"'}, type = "string"}, + {pattern = {"'", "'"}, type = "string"}, + {pattern = "[%a_][%w_]*%f[(]", type = "function"}, + {pattern = "[%a_][%w_]*", type = "symbol"}, + {pattern = "[%+%-=/%*%^%%<>!|&]", type = "operator"}, + {pattern = "0x[%da-fA-F]+", type = "number"}, + {pattern = "-?%d+[%d%.eE]*", type = "number"}, + {pattern = "-?%.?%d+", type = "number"}, + }, + symbols = { + ["TRUE"] = "literal", + ["FALSE"] = "literal", + ["NA"] = "literal", + ["NULL"] = "literal", + ["Inf"] = "literal", + ["if"] = "keyword", + ["else"] = "keyword", + ["while"] = "keyword", + ["function"] = "keyword", + ["break"] = "keyword", + ["next"] = "keyword", + ["repeat"] = "keyword", + ["in"] = "keyword", + ["for"] = "keyword", + ["NA_integer"] = "keyword", + ["NA_complex"] = "keyword", + ["NA_character"] = "keyword", + ["NA_real"] = "keyword" + } } diff --git a/plugins/language_angelscript.lua b/plugins/language_angelscript.lua index e62c1da..4e003ea 100644 --- a/plugins/language_angelscript.lua +++ b/plugins/language_angelscript.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_assembly_x86.lua b/plugins/language_assembly_x86.lua index baae3c4..e6d218b 100644 --- a/plugins/language_assembly_x86.lua +++ b/plugins/language_assembly_x86.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Support for the intel syntax x86-64 assembly -- Simply add it to lite-xl's plugins folder (located somewhere at home/.config/lite-xl/plugins) -- https://github.com/DMClVG diff --git a/plugins/language_batch.lua b/plugins/language_batch.lua index 13753cf..32510c8 100644 --- a/plugins/language_batch.lua +++ b/plugins/language_batch.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" -- batch syntax for lite <liqube> diff --git a/plugins/language_bib.lua b/plugins/language_bib.lua index cfde8da..1850ec6 100644 --- a/plugins/language_bib.lua +++ b/plugins/language_bib.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_caddyfile.lua b/plugins/language_caddyfile.lua new file mode 100644 index 0000000..f918c16 --- /dev/null +++ b/plugins/language_caddyfile.lua @@ -0,0 +1,104 @@ +-- mod-version:3 +local syntax = require "core.syntax" + +syntax.add { + files = { "Caddyfile" }, + comment = "#", + patterns = { + { pattern = { "#", "\n"}, type = "comment" }, + { pattern = { '"', '"', '\\' }, type = "string" }, + -- Matcher definition + { pattern = "@[%w_]+", type = "operator" }, + -- Snippet + { pattern = "%(%g+%)", type = "operator" }, + -- Properties + { pattern = "^[%a_][%w_]*()%s+%f[%g]", + type = { "function", "normal" } + }, + { pattern = "^[%a_][%w_]*()%s+$", + type = { "function", "normal" } + }, + { pattern = "^%s*()[%a_][%w_]*()%s+$", + type = { "normal", "function", "normal" } + }, + { pattern = "^%s*()[%a_][%w_]*()%s+%f[%g]", + type = { "normal", "function", "normal" } + }, + -- Environment variables + { pattern = "{()%$[%w_]+():()[%w_]+()}", + type = { "operator", "keyword2", "operator", "keyword2", "operator" } + }, + { pattern = "{()%$[%w_]+()}", + type = { "operator", "keyword2", "operator" } + }, + -- Place holder + { pattern = "{%g-}", type = "keyword2" }, + -- Operators + { pattern = "[+%-,:]", type = "operator" }, + -- IP Address + { pattern = "%d+%.%d+%.%d+%.%d+", type = "literal" }, + -- Path /path/subpath + { pattern = "/[%w%./]+", type = "literal" }, + -- Wildcard domain *.levels + { pattern = "%*()[%w.]+", + type = { "operator", "literal" } + }, + -- Match Operator + { pattern = "%*+", type = "operator" }, + -- Domain leve1.level2 + { pattern = "https?://[%w%./%*]+", type = "literal" }, + -- Domain leve1.level2 + { pattern = "%w+%.[%w%.]+", type = "literal" }, + -- Number + { pattern = "%d+[mhskbi]*", type = "number" }, + -- Everything else for symbols to work + { pattern = "[%a_][%w_]*", type = "symbol" }, + }, + symbols = { + ["true"] = "literal", + ["false"] = "literal", + ["localhost"] = "literal", + + -- built-in directives + ["abort"] = "keyword", + ["acme_server"] = "keyword", + ["basicauth"] = "keyword", + ["bind"] = "keyword", + ["encode"] = "keyword", + ["error"] = "keyword", + ["file_server"] = "keyword", + ["forward_auth"] = "keyword", + ["handle"] = "keyword", + ["handle_errors"] = "keyword", + ["handle_path"] = "keyword", + ["header"] = "keyword", + ["import"] = "keyword", + ["log"] = "keyword", + ["method"] = "keyword", + ["map"] = "keyword", + ["metrics"] = "keyword", + ["php_fastcgi"] = "keyword", + ["push"] = "keyword", + ["redir"] = "keyword", + ["request_body"] = "keyword", + ["request_header"] = "keyword", + ["respond"] = "keyword", + ["reverse_proxy"] = "keyword", + ["rewrite"] = "keyword", + ["root"] = "keyword", + ["route"] = "keyword", + ["templates"] = "keyword", + ["tls"] = "keyword", + ["tracing"] = "keyword", + ["try_files"] = "keyword", + ["uri"] = "keyword", + ["vars"] = "keyword", + + -- Module directives + ["cgi"] = "keyword", + ["ssh"] = "keyword", + ["exec"] = "keyword", + ["supervisor"] = "keyword", + ["layer4"] = "keyword", + }, +} diff --git a/plugins/language_cmake.lua b/plugins/language_cmake.lua index 8103632..19f1aa5 100644 --- a/plugins/language_cmake.lua +++ b/plugins/language_cmake.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_cpp.lua b/plugins/language_cpp.lua deleted file mode 100644 index b1afa0f..0000000 --- a/plugins/language_cpp.lua +++ /dev/null @@ -1,203 +0,0 @@ --- mod-version:2 -- lite-xl 2.0 -local syntax = require "core.syntax" - -syntax.add { - name = "C++", - files = { - "%.h$", "%.inl$", "%.cpp$", "%.cc$", "%.C$", "%.cxx$", - "%.c++$", "%.hh$", "%.H$", "%.hxx$", "%.hpp$", "%.h++$" - }, - comment = "//", - block_comment = { "/*", "*/" }, - patterns = { - { pattern = "//.-\n", type = "comment" }, - { pattern = { "/%*", "%*/" }, type = "comment" }, - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = "0x%x+", type = "number" }, - { pattern = "%d+[%d%.'eE]*f?", type = "number" }, - { pattern = "%.?%d+f?", type = "number" }, - { pattern = "[%+%-=/%*%^%%<>!~|:&]", type = "operator" }, - { pattern = "##", type = "operator" }, - { pattern = "struct%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - { pattern = "class%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - { pattern = "union%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - { pattern = "namespace%s()[%a_][%w_]*", type = {"keyword", "keyword2"} }, - -- static declarations - { pattern = "static()%s+()inline", - type = { "keyword", "normal", "keyword" } - }, - { pattern = "static()%s+()const", - type = { "keyword", "normal", "keyword" } - }, - { pattern = "static()%s+()[%a_][%w_]*", - type = { "keyword", "normal", "literal" } - }, - -- match method type declarations - { pattern = "[%a_][%w_]*()%s*()%**()%s*()[%a_][%w_]*()%s*()::", - type = { - "literal", "normal", "operator", "normal", - "literal", "normal", "operator" - } - }, - -- match function type declarations - { pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*%f[%(]", - type = { "literal", "operator", "normal", "function" } - }, - { pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*%f[%(]", - type = { "literal", "normal", "operator", "function" } - }, - { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*%f[%(]", - type = { "literal", "normal", "function" } - }, - -- match variable type declarations - { pattern = "[%a_][%w_]*()%*+()%s+()[%a_][%w_]*", - type = { "literal", "operator", "normal", "normal" } - }, - { pattern = "[%a_][%w_]*()%s+()%*+()[%a_][%w_]*", - type = { "literal", "normal", "operator", "normal" } - }, - { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()[;,%[%)]", - type = { "literal", "normal", "normal", "normal", "normal" } - }, - { pattern = "[%a_][%w_]*()%s+()[%a_][%w_]*()%s*()=", - type = { "literal", "normal", "normal", "normal", "operator" } - }, - { pattern = "[%a_][%w_]*()&()%s+()[%a_][%w_]*", - type = { "literal", "operator", "normal", "normal" } - }, - { pattern = "[%a_][%w_]*()%s+()&()[%a_][%w_]*", - type = { "literal", "normal", "operator", "normal" } - }, - -- Match scope operator element access - { pattern = "[%a_][%w_]*()%s*()::", - type = { "literal", "normal", "operator" } - }, - -- Uppercase constants of at least 2 chars in len - { pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]", - type = "number" - }, - -- Magic constants - { pattern = "__[%u%l]+__", type = "number" }, - -- all other functions - { pattern = "[%a_][%w_]*%f[(]", type = "function" }, - -- Macros - { pattern = "^%s*#%s*define%s+()[%a_][%a%d_]*", - type = { "keyword", "symbol" } - }, - { pattern = "#%s*include%s+()<.->", - type = { "keyword", "string" } - }, - { pattern = "%f[#]#%s*[%a_][%w_]*", type = "keyword" }, - -- Everything else to make the tokenizer work properly - { pattern = "[%a_][%w_]*", type = "symbol" }, - }, - symbols = { - ["alignof"] = "keyword", - ["alignas"] = "keyword", - ["and"] = "keyword", - ["and_eq"] = "keyword", - ["not"] = "keyword", - ["not_eq"] = "keyword", - ["or"] = "keyword", - ["or_eq"] = "keyword", - ["xor"] = "keyword", - ["xor_eq"] = "keyword", - ["private"] = "keyword", - ["protected"] = "keyword", - ["public"] = "keyword", - ["register"] = "keyword", - ["nullptr"] = "keyword", - ["operator"] = "keyword", - ["asm"] = "keyword", - ["bitand"] = "keyword", - ["bitor"] = "keyword", - ["catch"] = "keyword", - ["throw"] = "keyword", - ["try"] = "keyword", - ["class"] = "keyword", - ["compl"] = "keyword", - ["explicit"] = "keyword", - ["export"] = "keyword", - ["concept"] = "keyword", - ["consteval"] = "keyword", - ["constexpr"] = "keyword", - ["constinit"] = "keyword", - ["const_cast"] = "keyword", - ["dynamic_cast"] = "keyword", - ["reinterpret_cast"] = "keyword", - ["static_cast"] = "keyword", - ["static_assert"] = "keyword", - ["template"] = "keyword", - ["this"] = "keyword", - ["thread_local"] = "keyword", - ["requires"] = "keyword", - ["co_wait"] = "keyword", - ["co_return"] = "keyword", - ["co_yield"] = "keyword", - ["decltype"] = "keyword", - ["delete"] = "keyword", - ["friend"] = "keyword", - ["typeid"] = "keyword", - ["typename"] = "keyword", - ["mutable"] = "keyword", - ["override"] = "keyword", - ["virtual"] = "keyword", - ["using"] = "keyword", - ["namespace"] = "keyword", - ["new"] = "keyword", - ["noexcept"] = "keyword", - ["if"] = "keyword", - ["then"] = "keyword", - ["else"] = "keyword", - ["elseif"] = "keyword", - ["do"] = "keyword", - ["while"] = "keyword", - ["for"] = "keyword", - ["break"] = "keyword", - ["continue"] = "keyword", - ["return"] = "keyword", - ["goto"] = "keyword", - ["struct"] = "keyword", - ["union"] = "keyword", - ["typedef"] = "keyword", - ["enum"] = "keyword", - ["extern"] = "keyword", - ["static"] = "keyword", - ["volatile"] = "keyword", - ["const"] = "keyword", - ["inline"] = "keyword", - ["switch"] = "keyword", - ["case"] = "keyword", - ["default"] = "keyword", - ["auto"] = "keyword", - ["void"] = "keyword2", - ["int"] = "keyword2", - ["short"] = "keyword2", - ["long"] = "keyword2", - ["float"] = "keyword2", - ["double"] = "keyword2", - ["char"] = "keyword2", - ["unsigned"] = "keyword2", - ["bool"] = "keyword2", - ["true"] = "literal", - ["false"] = "literal", - ["NULL"] = "literal", - ["wchar_t"] = "keyword2", - ["char8_t"] = "keyword2", - ["char16_t"] = "keyword2", - ["char32_t"] = "keyword2", - ["#include"] = "keyword", - ["#if"] = "keyword", - ["#ifdef"] = "keyword", - ["#ifndef"] = "keyword", - ["#elif"] = "keyword", - ["#else"] = "keyword", - ["#elseif"] = "keyword", - ["#endif"] = "keyword", - ["#define"] = "keyword", - ["#warning"] = "keyword", - ["#error"] = "keyword", - ["#pragma"] = "keyword", - }, -} diff --git a/plugins/language_csharp.lua b/plugins/language_csharp.lua index 5e1e81a..c137b63 100644 --- a/plugins/language_csharp.lua +++ b/plugins/language_csharp.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_d.lua b/plugins/language_d.lua index e59916e..6788f0b 100644 --- a/plugins/language_d.lua +++ b/plugins/language_d.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_dart.lua b/plugins/language_dart.lua index 97aa375..03274b2 100644 --- a/plugins/language_dart.lua +++ b/plugins/language_dart.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_diff.lua b/plugins/language_diff.lua index 4376b26..c4c5a90 100644 --- a/plugins/language_diff.lua +++ b/plugins/language_diff.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" local style = require "core.style" local common = require "core.common" diff --git a/plugins/language_elixir.lua b/plugins/language_elixir.lua index f414bd4..8f47770 100644 --- a/plugins/language_elixir.lua +++ b/plugins/language_elixir.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_elm.lua b/plugins/language_elm.lua index 65ddc1f..2d33813 100644 --- a/plugins/language_elm.lua +++ b/plugins/language_elm.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_erb.lua b/plugins/language_erb.lua index e63c7b0..d55b48c 100644 --- a/plugins/language_erb.lua +++ b/plugins/language_erb.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_fe.lua b/plugins/language_fe.lua index 18400ac..aee9b85 100644 --- a/plugins/language_fe.lua +++ b/plugins/language_fe.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_fennel.lua b/plugins/language_fennel.lua index d2fa7f0..dcfd245 100644 --- a/plugins/language_fennel.lua +++ b/plugins/language_fennel.lua @@ -1,102 +1,268 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Support for the Fennel programming language: https://fennel-lang.org --- Covers all the keywords up to Fennel version 0.4.0 +-- Covers all the keywords up to Fennel version 1.2.0 -- Currently only covers highlighting, not indentation, delimiter -- matching, or evaluation. -local syntax = require "core.syntax" +local syntax = require("core.syntax") -syntax.add { - name = "Fennel", - files = "%.fnl$", - comment = ";", - patterns = { - { pattern = ";.-\n", type = "comment" }, - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = "0x[%da-fA-F]+", type = "number" }, - { pattern = "-?%d+[%d%.]*", type = "number" }, - { pattern = "-?%.?%d+", type = "number" }, - { pattern = "%f[^(][^()'%s\"]+", type = "function" }, - { pattern = "[^()'%s\"]+", type = "symbol" }, - }, +syntax.add({ + comment = ";", + files = "%.fnl$", + name = "Fennel", + patterns = { + { pattern = ";.-\n", type = "comment" }, + { pattern = { '"', '"', "\\" }, type = "string" }, + { pattern = "0x[%da-fA-F]+", type = "number" }, + { pattern = "-?%d+[%d%.]*", type = "number" }, + { pattern = "-?%.?%d+", type = "number" }, + { pattern = "%f[^(][^()'%s\"]+", type = "function" }, + { pattern = "[^()'%s\"]+", type = "symbol" }, + }, + symbols = { + ["#"] = "keyword", + ["%"] = "keyword", + ["*"] = "keyword", + ["+"] = "keyword", + ["-"] = "keyword", + ["->"] = "keyword", + ["->>"] = "keyword", + ["-?>"] = "keyword", + ["-?>>"] = "keyword", + ["."] = "keyword", + [".."] = "keyword", + ["/"] = "keyword", + ["//"] = "keyword", + [":"] = "keyword", + ["<"] = "keyword", + ["<="] = "keyword", + ["="] = "keyword", + [">"] = "keyword", + [">="] = "keyword", + ["?."] = "keyword", + ["^"] = "keyword", + _G = "keyword", + accumulate = "keyword2", + ["and"] = "keyword2", + arg = "keyword2", + assert = "keyword2", + band = "keyword2", + bit32 = "keyword2", + ["bit32.arshift"] = "keyword2", + ["bit32.band"] = "keyword2", + ["bit32.bnot"] = "keyword2", + ["bit32.bor"] = "keyword2", + ["bit32.btest"] = "keyword2", + ["bit32.bxor"] = "keyword2", + ["bit32.extract"] = "keyword2", + ["bit32.lrotate"] = "keyword2", + ["bit32.lshift"] = "keyword2", + ["bit32.replace"] = "keyword2", + ["bit32.rrotate"] = "keyword2", + ["bit32.rshift"] = "keyword2", + bnot = "keyword2", + bor = "keyword2", + bxor = "keyword2", + collect = "keyword2", + collectgarbage = "keyword2", + comment = "keyword2", + coroutine = "keyword2", + ["coroutine.create"] = "keyword2", + ["coroutine.resume"] = "keyword2", + ["coroutine.running"] = "keyword2", + ["coroutine.status"] = "keyword2", + ["coroutine.wrap"] = "keyword2", + ["coroutine.yield"] = "keyword2", + debug = "keyword2", + ["debug.debug"] = "keyword2", + ["debug.gethook"] = "keyword2", + ["debug.getinfo"] = "keyword2", + ["debug.getlocal"] = "keyword2", + ["debug.getmetatable"] = "keyword2", + ["debug.getregistry"] = "keyword2", + ["debug.getupvalue"] = "keyword2", + ["debug.getuservalue"] = "keyword2", + ["debug.sethook"] = "keyword2", + ["debug.setlocal"] = "keyword2", + ["debug.setmetatable"] = "keyword2", + ["debug.setupvalue"] = "keyword2", + ["debug.setuservalue"] = "keyword2", + ["debug.traceback"] = "keyword2", + ["debug.upvalueid"] = "keyword2", + ["debug.upvaluejoin"] = "keyword2", + ["do"] = "keyword2", + dofile = "keyword2", + doto = "keyword2", + each = "keyword2", + error = "keyword2", + ["eval-compiler"] = "keyword2", + ["false"] = "literal", + fcollect = "keyword2", + fn = "keyword2", + ["for"] = "keyword2", + getmetatable = "keyword2", + global = "keyword2", + hashfn = "keyword2", + icollect = "keyword2", + ["if"] = "keyword2", + ["import-macros"] = "keyword2", + include = "keyword2", + io = "keyword2", + ["io.close"] = "keyword2", + ["io.flush"] = "keyword2", + ["io.input"] = "keyword2", + ["io.lines"] = "keyword2", + ["io.open"] = "keyword2", + ["io.output"] = "keyword2", + ["io.popen"] = "keyword2", + ["io.read"] = "keyword2", + ["io.tmpfile"] = "keyword2", + ["io.type"] = "keyword2", + ["io.write"] = "keyword2", + ipairs = "keyword2", + lambda = "keyword2", + length = "keyword2", + let = "keyword2", + load = "keyword2", + loadfile = "keyword2", + loadstring = "keyword2", + ["local"] = "keyword2", + lshift = "keyword2", + lua = "keyword2", + macro = "keyword2", + macrodebug = "keyword2", + macros = "keyword2", + match = "keyword2", + ["match-try"] = "keyword2", + math = "keyword2", + ["math.abs"] = "keyword2", + ["math.acos"] = "keyword2", + ["math.asin"] = "keyword2", + ["math.atan"] = "keyword2", + ["math.atan2"] = "keyword2", + ["math.ceil"] = "keyword2", + ["math.cos"] = "keyword2", + ["math.cosh"] = "keyword2", + ["math.deg"] = "keyword2", + ["math.exp"] = "keyword2", + ["math.floor"] = "keyword2", + ["math.fmod"] = "keyword2", + ["math.frexp"] = "keyword2", + ["math.ldexp"] = "keyword2", + ["math.log"] = "keyword2", + ["math.log10"] = "keyword2", + ["math.max"] = "keyword2", + ["math.min"] = "keyword2", + ["math.modf"] = "keyword2", + ["math.pow"] = "keyword2", + ["math.rad"] = "keyword2", + ["math.random"] = "keyword2", + ["math.randomseed"] = "keyword2", + ["math.sin"] = "keyword2", + ["math.sinh"] = "keyword2", + ["math.sqrt"] = "keyword2", + ["math.tan"] = "keyword2", + ["math.tanh"] = "keyword2", + module = "keyword2", + next = "keyword2", + ["nil"] = "literal", + ["not"] = "keyword2", + ["not="] = "keyword2", + ["or"] = "keyword2", + os = "keyword2", + ["os.clock"] = "keyword2", + ["os.date"] = "keyword2", + ["os.difftime"] = "keyword2", + ["os.execute"] = "keyword2", + ["os.exit"] = "keyword2", + ["os.getenv"] = "keyword2", + ["os.remove"] = "keyword2", + ["os.rename"] = "keyword2", + ["os.setlocale"] = "keyword2", + ["os.time"] = "keyword2", + ["os.tmpname"] = "keyword2", + package = "keyword2", + ["package.loadlib"] = "keyword2", + ["package.searchpath"] = "keyword2", + ["package.seeall"] = "keyword2", + pairs = "keyword2", + partial = "keyword2", + pcall = "keyword2", + ["pick-args"] = "keyword2", + ["pick-values"] = "keyword2", + print = "keyword2", + quote = "keyword2", + rawequal = "keyword2", + rawget = "keyword2", + rawlen = "keyword2", + rawset = "keyword2", + require = "keyword2", + ["require-macros"] = "keyword2", + rshift = "keyword2", + select = "keyword2", + set = "keyword2", + ["set-forcibly!"] = "keyword2", + setmetatable = "keyword2", + string = "keyword2", + ["string.byte"] = "keyword2", + ["string.char"] = "keyword2", + ["string.dump"] = "keyword2", + ["string.find"] = "keyword2", + ["string.format"] = "keyword2", + ["string.gmatch"] = "keyword2", + ["string.gsub"] = "keyword2", + ["string.len"] = "keyword2", + ["string.lower"] = "keyword2", + ["string.match"] = "keyword2", + ["string.rep"] = "keyword2", + ["string.reverse"] = "keyword2", + ["string.sub"] = "keyword2", + ["string.upper"] = "keyword2", + table = "keyword2", + ["table.concat"] = "keyword2", + ["table.insert"] = "keyword2", + ["table.maxn"] = "keyword2", + ["table.pack"] = "keyword2", + ["table.remove"] = "keyword2", + ["table.sort"] = "keyword2", + ["table.unpack"] = "keyword2", + tonumber = "keyword2", + tostring = "keyword2", + ["true"] = "literal", + tset = "keyword2", + type = "keyword2", + unpack = "keyword2", + values = "keyword2", + var = "keyword2", + when = "keyword2", + ["while"] = "keyword2", + ["with-open"] = "keyword2", + xpcall = "keyword2", + ["~="] = "keyword", + ["λ"] = "keyword", + }, +}) - symbols = { - ["eval-compiler"] = "keyword2", - ["doc"] = "keyword2", - ["lua"] = "keyword2", - ["hashfn"] = "keyword2", - ["macro"] = "keyword2", - ["macros"] = "keyword2", - ["import-macros"] = "keyword2", - ["do"] = "keyword2", - ["values"] = "keyword2", - ["if"] = "keyword2", - ["when"] = "keyword2", - ["each"] = "keyword2", - ["for"] = "keyword2", - ["fn"] = "keyword2", - ["lambda"] = "keyword2", - ["λ"] = "keyword2", - ["partial"] = "keyword2", - ["while"] = "keyword2", - ["set"] = "keyword2", - ["global"] = "keyword2", - ["var"] = "keyword2", - ["local"] = "keyword2", - ["let"] = "keyword2", - ["tset"] = "keyword2", - ["set-forcibly!"] = "keyword2", - ["doto"] = "keyword2", - ["match"] = "keyword2", - ["or"] = "keyword2", - ["and"] = "keyword2", - ["not"] = "keyword2", - ["not="] = "keyword2", - ["pick-args"] = "keyword2", - ["pick-values"] = "keyword2", - ["macrodebug"] = "keyword2", +-- To regenerate the syntax from the compiler: +-- (macro s [] +-- (let [{: syntax} (require :fennel) +-- symbols {:nil :literal +-- :true :literal +-- :false :literal}] +-- `(syntax.add {:name "Fennel" +-- :files "%.fnl$" +-- :comment ";" +-- :patterns [{:type :comment :pattern ";.-\n"} +-- {:type :string :pattern {1 "\"" 2 "\"" 3 "\\"}} +-- {:type :number :pattern "0x[%da-fA-F]+"} +-- {:type :number :pattern "-?%d+[%d%.]*"} +-- {:type :number :pattern "-?%.?%d+"} +-- {:type :function :pattern "%f[^(][^()'%s\"]+"} +-- {:type :symbol :pattern "[^()'%s\"]+"}] +-- :symbols ,(collect [name (pairs (syntax)) :into symbols] +-- (values name +-- (if (name:find "[a-z]") +-- :keyword2 :keyword)))}))) (s) - ["."] = "keyword", - ["+"] = "keyword", - [".."] = "keyword", - ["^"] = "keyword", - ["-"] = "keyword", - ["*"] = "keyword", - ["%"] = "keyword", - ["/"] = "keyword", - [">"] = "keyword", - ["<"] = "keyword", - [">="] = "keyword", - ["<="] = "keyword", - ["="] = "keyword", - ["#"] = "keyword", - ["..."] = "keyword", - [":"] = "keyword", - ["->"] = "keyword", - ["->>"] = "keyword", - ["-?>"] = "keyword", - ["-?>>"] = "keyword", - ["$"] = "keyword", - ["$1"] = "keyword", - ["$2"] = "keyword", - ["$3"] = "keyword", - ["$4"] = "keyword", - ["$5"] = "keyword", - ["$6"] = "keyword", - ["$7"] = "keyword", - ["$8"] = "keyword", - ["$9"] = "keyword", - - ["lshift"] = "keyword", - ["rshift"] = "keyword", - ["bor"] = "keyword", - ["band"] = "keyword", - ["bnot"] = "keyword", - ["bxor"] = "keyword", - - ["nil"] = "literal", - ["true"] = "literal", - ["false"] = "literal", - } -} +-- and reformat the output, of course diff --git a/plugins/language_fstab.lua b/plugins/language_fstab.lua index 4198516..bbe418e 100644 --- a/plugins/language_fstab.lua +++ b/plugins/language_fstab.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local syntax = require "core.syntax" @@ -7,7 +7,7 @@ syntax.add { files = { "fstab" }, comment = '#', patterns = { - -- Only lines that start with a # are comments; you can have #'s in fuse + -- Only lines that start with a # are comments; you can have #'s in fuse -- filesystem strings that aren't comments, so shouldn't be highlighted as such. { regex = "^#.*$", type = "comment" }, { pattern = "[=/:.,]+", type = "operator" }, @@ -45,7 +45,7 @@ syntax.add { ["LABEL"] = "keyword", ["UUID"] = "keyword", - + -- filesystems ["aufs"] = "keyword2", ["autofs"] = "keyword2", diff --git a/plugins/language_gdscript.lua b/plugins/language_gdscript.lua index 168fc44..d03dce8 100644 --- a/plugins/language_gdscript.lua +++ b/plugins/language_gdscript.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Support for the GDScript programming language: https://godotengine.org/ -- Covers the most used keywords up to Godot version 3.2.x diff --git a/plugins/language_glsl.lua b/plugins/language_glsl.lua index f8b2782..360e11f 100644 --- a/plugins/language_glsl.lua +++ b/plugins/language_glsl.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local style = require "core.style" local common = require "core.common" diff --git a/plugins/language_gmi.lua b/plugins/language_gmi.lua index a6ef4d8..c5d819a 100644 --- a/plugins/language_gmi.lua +++ b/plugins/language_gmi.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" @@ -10,7 +10,7 @@ syntax.add { { pattern = { "```", "```" }, type = "string" }, { pattern = "#.*", type = "keyword" }, { pattern = "%*%s", type = "keyword2" }, - { pattern = "=>", type = "function" }, + { pattern = "=>", type = "function" }, { pattern = "https?://%S+", type = "literal" }, { pattern = "gemini?://%S+", type = "literal" }, { pattern = ">.*", type = "comment" }, diff --git a/plugins/language_go.lua b/plugins/language_go.lua index 58c38c2..93db0e4 100644 --- a/plugins/language_go.lua +++ b/plugins/language_go.lua @@ -1,26 +1,143 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { name = "Go", files = { "%.go$" }, comment = "//", + block_comment = {"/*", "*/"}, patterns = { - { pattern = "//.-\n", type = "comment" }, - { pattern = { "/%*", "%*/" }, type = "comment" }, - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = { "`", "`", '\\' }, type = "string" }, - { pattern = { "'", "'", '\\' }, type = "string" }, - { pattern = "0[oO_][0-7]+", type = "number" }, - { pattern = "-?0x[%x_]+", type = "number" }, - { pattern = "-?%d+_%d", type = "number" }, - { pattern = "-?%d+[%d%.eE]*f?", type = "number" }, - { pattern = "-?%.?%d+f?", type = "number" }, - { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, - { pattern = ":=", type = "operator" }, - { pattern = "[%a_][%w_]*%f[(]", type = "function" }, -- function call - { pattern = "func()%s*[%a_][%w_]*()%f[%[(]", type = {"keyword", "function", "normal"} }, -- function statement - { pattern = "[%a_][%w_]*", type = "symbol" }, + { pattern = "//.-\n", type = "comment" }, + { pattern = { "/%*", "%*/" }, type = "comment" }, + { pattern = { '"', '"', '\\' }, type = "string" }, + { pattern = { "`", "`", '\\' }, type = "string" }, + { pattern = { "'", "'", '\\' }, type = "string" }, + { pattern = "0[oO_][0-7]+i?", type = "number" }, + { pattern = "-?0x[%x_]+i?", type = "number" }, + { pattern = "-?%d+_%di?", type = "number" }, + { pattern = "-?%d+[%d%.eE]*f?i?", type = "number" }, + { pattern = "-?%.?%d+f?i?", type = "number" }, + -- goto label + { pattern = "^%s+()[%a_][%w%_]*()%s*:%s$", -- this is to fix `default:` + type = { "normal", "function", "normal" } + }, + { pattern = "^%s*[%a_][%w%_]*()%s*:%s$", + type = { "function", "normal" } + }, + -- pointer, generic and reference type + { pattern = "[%*~&]()[%a_][%w%_]*", + type = { "operator", "keyword2" } + }, + -- slice type + { pattern = "%[%]()[%a_][%w%_]*", + type = { "operator", "keyword2" } + }, + -- type coerce + { + pattern = "%.%(()[%a_][%w_]*()%)", + type = { "normal", "keyword2", "normal" } + }, + -- struct literal + { pattern = "[%a_][%w%_]*()%s*{%s*", + type = { "keyword2", "normal" } + }, + -- operators + { pattern = "[%+%-=/%*%^%%<>!~|&]", type = "operator" }, + { pattern = ":=", type = "operator" }, + -- function calls + { pattern = "func()%s*[%a_][%w_]*()%f[%[(]", -- function statement + type = {"keyword", "function", "normal"} + }, + { pattern = "[%a_][%w_]*%f[(]", type = "function" }, + { pattern = "%.()[%a_][%w_]*%f[(]", + type = { "normal", "function" } + }, + -- type declaration + { pattern = "type()%s+()[%a_][%w%_]*", + type = { "keyword", "normal", "keyword2" } + }, + -- variable declaration + { pattern = "var()%s+()[%a_][%w%_]*", + type = { "keyword", "normal", "symbol" } + }, + -- goto + { pattern = "goto()%s+()[%a_][%w%_]*", + type = { "keyword", "normal", "function" } + }, + -- if fix + { pattern = "if()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- for fix + { pattern = "for()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- return fix + { pattern = "return()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- range fix + { pattern = "range()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- func fix + { pattern = "func()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- switch fix + { pattern = "switch()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- case fix + { pattern = "case()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- break fix + { pattern = "break()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- continue fix + { pattern = "continue()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- package fix + { pattern = "package()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- go fix + { pattern = "go()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- chan fix + { pattern = "chan()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- defer fix + { pattern = "defer()%s+%f[%a_]", + type = { "keyword", "normal" } + }, + -- field declaration + { pattern = "[%a_][%w%_]*()%s*():%s*%f[%w%p]", + type = { "function", "normal", "operator" } + }, + -- parameters or declarations + { pattern = "[%a_][%w%_]*()%s+()[%*~&]?()[%a_][%w%_]*", + type = { "literal", "normal", "operator", "keyword2" } + }, + { pattern = "[%a_][%w_]*()%s+()%[%]()[%a_][%w%_]*", + type = { "literal", "normal", "normal", "keyword2" } + }, + -- single return type + { + pattern = "%)%s+%(?()[%a_][%w%_]*()%)?%s+%{", + type = { "normal", "keyword2", "normal" } + }, + -- sub fields + { pattern = "%.()[%a_][%w_]*", + type = { "normal", "literal" } + }, + -- every other symbol + { pattern = "[%a_][%w_]*", type = "symbol" }, }, symbols = { ["if"] = "keyword", @@ -48,6 +165,7 @@ syntax.add { ["go"] = "keyword", ["fallthrough"] = "keyword", ["goto"] = "keyword", + ["iota"] = "keyword2", ["int"] = "keyword2", ["int64"] = "keyword2", ["int32"] = "keyword2", diff --git a/plugins/language_hlsl.lua b/plugins/language_hlsl.lua index 696e9b2..e1d5570 100644 --- a/plugins/language_hlsl.lua +++ b/plugins/language_hlsl.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local style = require "core.style" local common = require "core.common" diff --git a/plugins/language_hs.lua b/plugins/language_hs.lua index a60ff75..4271210 100644 --- a/plugins/language_hs.lua +++ b/plugins/language_hs.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_htaccess.lua b/plugins/language_htaccess.lua index 2cb71ca..acfa419 100644 --- a/plugins/language_htaccess.lua +++ b/plugins/language_htaccess.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" local keywords = { diff --git a/plugins/language_ini.lua b/plugins/language_ini.lua index 706cc80..2f5d3b5 100644 --- a/plugins/language_ini.lua +++ b/plugins/language_ini.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local syntax = require "core.syntax" diff --git a/plugins/language_java.lua b/plugins/language_java.lua index 810ffd2..30c1249 100644 --- a/plugins/language_java.lua +++ b/plugins/language_java.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_jiyu.lua b/plugins/language_jiyu.lua index 78cc377..dbf9d0e 100644 --- a/plugins/language_jiyu.lua +++ b/plugins/language_jiyu.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_jsx.lua b/plugins/language_jsx.lua index 1cbc4ac..04e846b 100644 --- a/plugins/language_jsx.lua +++ b/plugins/language_jsx.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Almost identical to JS, with the exception that / shouldn't denote a regex. Current JS syntax highlighter will highlight half the document due to closing tags. local syntax = require "core.syntax" diff --git a/plugins/language_julia.lua b/plugins/language_julia.lua index e62a9b2..347592f 100644 --- a/plugins/language_julia.lua +++ b/plugins/language_julia.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Support for the Julia programming language: -- Covers the most used keywords up to Julia version 1.6.4 diff --git a/plugins/language_liquid.lua b/plugins/language_liquid.lua index 58688c8..ad40a79 100644 --- a/plugins/language_liquid.lua +++ b/plugins/language_liquid.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" local liquid_syntax = { @@ -92,7 +92,7 @@ local liquid_syntax = { ["upcase"] = "keyword2", ["when"] = "keyword", ["where"] = "keyword2" - + }, } @@ -102,27 +102,27 @@ syntax.add { patterns = { { pattern = { "{%%", "%%}" }, syntax = liquid_syntax, type = "function" }, { pattern = { "{{", "}}" }, syntax = liquid_syntax, type = "function" }, - { - pattern = { + { + pattern = { "<%s*[sS][cC][rR][iI][pP][tT]%s+[tT][yY][pP][eE]%s*=%s*" .. "['\"]%a+/[jJ][aA][vV][aA][sS][cC][rR][iI][pP][tT]['\"]%s*>", - "<%s*/[sS][cC][rR][iI][pP][tT]>" + "<%s*/[sS][cC][rR][iI][pP][tT]>" }, - syntax = ".js", - type = "function" + syntax = ".js", + type = "function" }, - { - pattern = { + { + pattern = { "<%s*[sS][cC][rR][iI][pP][tT]%s*>", - "<%s*/%s*[sS][cC][rR][iI][pP][tT]>" + "<%s*/%s*[sS][cC][rR][iI][pP][tT]>" }, syntax = ".js", type = "function" }, - { - pattern = { - "<%s*[sS][tT][yY][lL][eE][^>]*>", - "<%s*/%s*[sS][tT][yY][lL][eE]%s*>" + { + pattern = { + "<%s*[sS][tT][yY][lL][eE][^>]*>", + "<%s*/%s*[sS][tT][yY][lL][eE]%s*>" }, syntax = ".css", type = "function" diff --git a/plugins/language_lobster.lua b/plugins/language_lobster.lua index b6ab143..24f4251 100644 --- a/plugins/language_lobster.lua +++ b/plugins/language_lobster.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -46,7 +46,7 @@ syntax.add { ["string"] = "keyword2", ["any"] = "keyword2", ["void"] = "keyword2", - + ["is"] = "keyword", ["typeof"] = "keyword", ["var"] = "keyword", @@ -59,7 +59,7 @@ syntax.add { ["not"] = "operator", ["and"] = "operator", - ["or"] = "operator", + ["or"] = "operator", ["nil"] = "literal", }, diff --git a/plugins/language_make.lua b/plugins/language_make.lua index 687b827..4ad3521 100644 --- a/plugins/language_make.lua +++ b/plugins/language_make.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_meson.lua b/plugins/language_meson.lua index f5c55d7..be10d66 100644 --- a/plugins/language_meson.lua +++ b/plugins/language_meson.lua @@ -1,9 +1,9 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { name = "Meson", - files = "meson.build$", + files = { "^meson%.build$", "^meson_options%.txt$" }, comment = "#", patterns = { { pattern = { "#", "\n" }, type = "comment" }, diff --git a/plugins/language_miniscript.lua b/plugins/language_miniscript.lua new file mode 100644 index 0000000..afe0a2b --- /dev/null +++ b/plugins/language_miniscript.lua @@ -0,0 +1,102 @@ +-- mod-version:3 +local syntax = require 'core.syntax' + +syntax.add { + name = "MiniScript", + files = { "%.ms$" }, + comment = "//", + patterns = { + { pattern = "//.*", type = "comment" }, + { pattern = { '"', '"' }, type = "string" }, + { pattern = "[<>!=]=", type = "operator" }, + { pattern = "[%+%-%*%/%^@<>:]", type = "operator" }, + { pattern = "%d%.%d*[eE][-+]?%d+", type = "number" }, + { pattern = "%d%.%d*", type = "number" }, + { pattern = "%.?%d*[eE][-+]?%d+", type = "number" }, + { pattern = "%.?%d+", type = "number" }, + { pattern = "[%a_][%w_]*", type = "symbol" }, + }, + symbols = { + ["if"] = "keyword", + ["not"] = "keyword", + ["and"] = "keyword", + ["or"] = "keyword", + ["else"] = "keyword", + ["then"] = "keyword", + ["for"] = "keyword", + ["in"] = "keyword", + ["while"] = "keyword", + ["break"] = "keyword", + ["continue"] = "keyword", + ["function"] = "keyword", + ["end"] = "keyword", + ["return"] = "keyword", + ["new"] = "keyword", + ["isa"] = "keyword", + ["self"] = "keyword2", + + ["true"] = "literal", + ["false"] = "literal", + ["null"] = "literal", + ["globals"] = "literal", + ["locals"] = "literal", + ["outer"] = "literal", + + -- Built-in types's classes + ["number"] = "literal", + ["string"] = "literal", + ["list"] = "literal", + ["map"] = "literal", + ["funcRef"] = "literal", + + -- Intrinsic functions + ["abs"] = "function", + ["acos"] = "function", + ["asin"] = "function", + ["atan"] = "function", + ["bitAnd"] = "function", + ["bitOr"] = "function", + ["bitXor"] = "function", + ["ceil"] = "function", + ["char"] = "function", + ["code"] = "function", + ["cos"] = "function", + ["floor"] = "function", + ["hash"] = "function", + ["hasIndex"] = "function", + ["indexes"] = "function", + ["indexOf"] = "function", + ["insert"] = "function", + ["join"] = "function", + ["len"] = "function", + ["log"] = "function", + ["lower"] = "function", + ["pi"] = "function", + ["pop"] = "function", + ["print"] = "function", + ["pull"] = "function", + ["push"] = "function", + ["range"] = "function", + ["remove"] = "function", + ["replace"] = "function", + ["rnd"] = "function", + ["round"] = "function", + ["shuffle"] = "function", + ["sign"] = "function", + ["sin"] = "function", + ["slice"] = "function", + ["sort"] = "function", + ["split"] = "function", + ["sqrt"] = "function", + ["str"] = "function", + ["sum"] = "function", + ["tan"] = "function", + ["time"] = "function", + ["upper"] = "function", + ["val"] = "function", + ["values"] = "function", + ["version"] = "function", + ["wait"] = "function", + ["yield"] = "function", + }, +} diff --git a/plugins/language_moon.lua b/plugins/language_moon.lua index 59eea37..8042b7d 100644 --- a/plugins/language_moon.lua +++ b/plugins/language_moon.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_nginx.lua b/plugins/language_nginx.lua index 73cf979..7124c1a 100644 --- a/plugins/language_nginx.lua +++ b/plugins/language_nginx.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" -- Copied from https://github.com/shanoor/vscode-nginx/blob/master/syntaxes/nginx.tmLanguage diff --git a/plugins/language_nim.lua b/plugins/language_nim.lua index 5f00365..d7726d1 100644 --- a/plugins/language_nim.lua +++ b/plugins/language_nim.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" local patterns = {} diff --git a/plugins/language_objc.lua b/plugins/language_objc.lua index e0945d2..8d1210b 100644 --- a/plugins/language_objc.lua +++ b/plugins/language_objc.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_odin.lua b/plugins/language_odin.lua index ff75700..a5dfa45 100644 --- a/plugins/language_odin.lua +++ b/plugins/language_odin.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_perl.lua b/plugins/language_perl.lua index 1938b1b..0e057e2 100644 --- a/plugins/language_perl.lua +++ b/plugins/language_perl.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_php.lua b/plugins/language_php.lua index 5ef4f22..2820862 100644 --- a/plugins/language_php.lua +++ b/plugins/language_php.lua @@ -1,23 +1,152 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 --[[ language_php.lua provides php syntax support allowing mixed html, css and js - version: 20210902_1 + version: 20220614_1 --]] local syntax = require "core.syntax" +local common = require "core.common" +local config = require "core.config" -- load syntax dependencies to add additional rules require "plugins.language_css" require "plugins.language_js" --- generate SQL string marker regex -local sql_markers = { 'create', 'select', 'insert', 'update', 'replace', 'delete', 'drop', 'alter' } -local sql_regex = {} -for _,marker in ipairs(sql_markers) do - table.insert(sql_regex, marker) - table.insert(sql_regex, string.upper(marker)) +local psql_found = pcall(require, "plugins.language_psql") +local sql_strings = {} + +config.plugins.language_php = common.merge({ + sql_strings = true, + -- The config specification used by the settings gui + config_spec = { + name = "Language PHP", + { + label = "SQL Strings", + description = "Highlight as SQL, strings starting with sql statements, " + .. "depends on language_psql.", + path = "sql_strings", + type = "toggle", + default = true, + on_apply = function(enabled) + local syntax_php = syntax.get("file.phps") + if enabled and psql_found then + if + not syntax_php.patterns[6].syntax + or + syntax_php.patterns[6].syntax ~= '.sql' + then + table.insert(syntax_php.patterns, 5, sql_strings[1]) + table.insert(syntax_php.patterns, 6, sql_strings[2]) + end + elseif + syntax_php.patterns[6].syntax + and + syntax_php.patterns[6].syntax == '.sql' + then + table.remove(syntax_php.patterns, 5) + table.remove(syntax_php.patterns, 5) + end + end + } + } +}, config.plugins.language_php) + +-- Patterns to match some of the string inline variables +local inline_variables = { + { pattern = "%s+", type = "string" }, + { pattern = "\\%$", type = "string" }, + { pattern = "%{[%$%s]*%}", type = "string" }, + -- matches {$varname[index]} + { pattern = "{" + .. "()%$[%a_][%w_]*" + .. "()%[" + .. "()[%w%s_%-\"\'%(%)|;:,%.#@%%!%^&%*%+=%[%]<>~`%?\\/]*" + .. "()%]" + .. "}", + type = { + "keyword", "keyword2", "keyword", "string", "keyword" + } + }, + { pattern = "{" + .. "()%$[%a_][%w_]*" + .. "()%->" + .. "()[%a_][%w_]*" + .. "()}", + type = { + "keyword", "keyword2", "keyword", "symbol", "keyword" + } + }, + { pattern = "{()%$[%a_][%w_]*()}", + type = { "keyword", "keyword2", "keyword" } + }, + { pattern = "%$[%a_][%w_]*()%[()%w*()%]", + type = { "keyword2", "keyword", "string", "keyword" } + }, + { pattern = "%$[%a_][%w_]*()%->()%w+", + type = { "keyword2", "keyword", "symbol" } + }, + { pattern = "%$[%a_][%w_]*", type = "keyword2" }, + { pattern = "%w+", type = "string" }, + { pattern = "[^\"]", type = "string" }, +} + +local function combine_patterns(t1, t2) + local temp = { table.unpack(t1) } + for _, t in ipairs(t2) do + table.insert(temp, t) + end + return temp +end + +local function clone(tbl) + local t = {} + if tbl then + for k, v in pairs(tbl) do + if type(v) == "table" then + t[k] = clone(v) + else + t[k] = v + end + end + end + return t +end + +-- optionally allow sql syntax on strings +if psql_found then + -- generate SQL string marker regex + local sql_markers = { 'create', 'select', 'insert', 'update', 'replace', 'delete', 'drop', 'alter' } + local sql_regex = {} + for _, marker in ipairs(sql_markers) do + table.insert(sql_regex, marker) + table.insert(sql_regex, string.upper(marker)) + end + sql_regex = table.concat(sql_regex, '|') + + -- inject inline variable rules to cloned psql syntax + local syntax_phpsql = clone(syntax.get("file.sql")) + syntax_phpsql.name = "PHP SQL" + syntax_phpsql.files = "%.phpsql$" + table.insert(syntax_phpsql.patterns, 2, { pattern = "\\%$", type = "symbol" }) + table.insert(syntax_phpsql.patterns, 3, { pattern = "%{[%$%s]*%}", type = "symbol" }) + for i=4, 9 do + table.insert(syntax_phpsql.patterns, i, inline_variables[i]) + end + + -- SQL strings + sql_strings = { + { + regex = { '"(?=(?:'..sql_regex..')\\s+)', '"', '\\' }, + syntax = syntax_phpsql, + type = "string" + }, + { + regex = { '\'(?=(?:'..sql_regex..')\\s+)', '\'', '\\' }, + syntax = '.sql', + type = "string" + }, + } end -sql_regex = table.concat(sql_regex, '|') -- define the core php syntax coloring syntax.add { @@ -33,20 +162,36 @@ syntax.add { { pattern = "//.-\n", type = "comment" }, { pattern = "#.-\n", type = "comment" }, { pattern = { "/%*", "%*/" }, type = "comment" }, - -- SQL strings - { - regex = { '"(?=(?:'..sql_regex..')\\s+)', '"', '\\' }, - syntax = '.sql', - type = "string" + -- Single quote string + { pattern = { "'", "'", '\\' }, type = "string" }, + { pattern = { "<<<'%a%w*'\n", "^%s*%a%w*%f[;]", '\\' }, + type = "string" }, - { - regex = { '\'(?=(?:'..sql_regex..')\\s+)', '\'', '\\' }, - syntax = '.sql', - type = "string" + -- Strings with support for some inline variables syntax + { pattern = { "<<<%a%w*\n", "^%s*%a%w*%f[;]", '\\' }, + syntax = { + patterns = combine_patterns(inline_variables, { + -- prevent matching outside of the parent string + { pattern = "^%s*%a%w*();$", + type = { "string", "normal" } + }, + { pattern = "%p", type = "string" }, + }), + symbols = {} + }, + type = "string" + }, + { pattern = { '"', '"', '\\' }, + syntax = { + patterns = combine_patterns(inline_variables, { + -- prevent matching outside of the parent string + { pattern = "%p+%f[\"]", type = "string" }, + { pattern = "%p", type = "string" }, + }), + symbols = {} + }, + type = "string" }, - -- The '\\' is for escaping to work on " or ' - { pattern = { '"', '"', '\\' }, type = "string" }, - { pattern = { "'", "'", '\\' }, type = "string" }, { pattern = "0[bB][%d]+", type = "number" }, { pattern = "0[xX][%da-fA-F]+", type = "number" }, { pattern = "-?%d[%d_%.eE]*", type = "number" }, @@ -171,6 +316,13 @@ syntax.add { }, } +-- insert sql string rules after the "/%*", "%*/" pattern +if psql_found and config.plugins.language_php.sql_strings then + local syntax_php = syntax.get("file.phps") + table.insert(syntax_php.patterns, 5, sql_strings[1]) + table.insert(syntax_php.patterns, 6, sql_strings[2]) +end + -- allows html, css and js coloring on php files syntax.add { name = "PHP", diff --git a/plugins/language_pico8.lua b/plugins/language_pico8.lua index 40c9772..aefd6f6 100644 --- a/plugins/language_pico8.lua +++ b/plugins/language_pico8.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_pkgbuild.lua b/plugins/language_pkgbuild.lua index de38d5c..049a5d1 100644 --- a/plugins/language_pkgbuild.lua +++ b/plugins/language_pkgbuild.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_po.lua b/plugins/language_po.lua index db060b1..a1b098c 100644 --- a/plugins/language_po.lua +++ b/plugins/language_po.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_powershell.lua b/plugins/language_powershell.lua index 63de2f3..fdba844 100644 --- a/plugins/language_powershell.lua +++ b/plugins/language_powershell.lua @@ -1,74 +1,77 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { - name = "PowerShell", - files = {"%.ps1$", "%.psm1$", "%.psd1$", "%.ps1xml$", "%.pssc$", "%.psrc$", "%.cdxml$"}, - comment = "#", - patterns = { - {pattern = "#.*\n", type = "comment"}, - {pattern = [[\.]], type = "normal"}, - {pattern = {'"', '"'}, type = "string"}, - {pattern = {"'", "'"}, type = "string"}, - {pattern = "%f[%w_][%d%.]+%f[^%w_]", type = "number"}, - {pattern = "[%+=/%*%^%%<>!~|&,:]+", type = "operator"}, - {pattern = "%f[%S]%-[%w%-_]+", type = "function"}, - {pattern = "[%u][%a]+[%-][%u][%a]+", type = "function"}, - {pattern = "${.*}", type = "symbol"}, - {pattern = "$[%a_@*][%w_]*", type = "keyword2"}, - {pattern = "$[%$][%a]+", type = "keyword2"}, - {pattern = "[%a_][%w_]*", type = "symbol"} - }, - symbols = { - ["if"] = "keyword", - ["else"] = "keyword", - ["elseif"] = "keyword", - ["switch"] = "keyword", - ["default"] = "keyword", - ["function"] = "keyword", - ["filter"] = "keyword", - ["workflow"] = "keyword", - ["configuration"] = "keyword", - ["class"] = "keyword", - ["enum"] = "keyword", - ["Parameter"] = "keyword", - ["ValidateScript"] = "keyword", - ["CmdletBinding"] = "keyword", - ["try"] = "keyword", - ["catch"] = "keyword", - ["finally"] = "keyword", - ["throw"] = "keyword", - ["while"] = "keyword", - ["for"] = "keyword", - ["do"] = "keyword", - ["until"] = "keyword", - ["break"] = "keyword", - ["continue"] = "keyword", - ["foreach"] = "keyword", - ["in"] = "keyword", - ["return"] = "keyword", - ["where"] = "function", - ["select"] = "function", - ["filter"] = "keyword", - ["in"] = "keyword", - ["trap"] = "keyword", - ["param"] = "keyword", - ["data"] = "keyword", - ["dynamicparam"] = "keyword", - ["begin"] = "function", - ["process"] = "function", - ["end"] = "function", - ["exit"] = "function", - ["inlinescript"] = "function", - ["parallel"] = "function", - ["sequence"] = "function", - ["true"] = "literal", - ["false"] = "literal", - ["TODO"] = "comment", - ["FIXME"] = "comment", - ["XXX"] = "comment", - ["TBD"] = "comment", - ["HACK"] = "comment", - ["NOTE"] = "comment" - } + name = "PowerShell", + files = { + "%.ps1$", "%.psm1$", "%.psd1$", "%.ps1xml$", + "%.pssc$", "%.psrc$", "%.cdxml$" + }, + comment = "#", + patterns = { + {pattern = "#.*\n", type = "comment"}, + {pattern = [[\.]], type = "normal"}, + {pattern = {'"', '"'}, type = "string"}, + {pattern = {"'", "'"}, type = "string"}, + {pattern = "%f[%w_][%d%.]+%f[^%w_]", type = "number"}, + {pattern = "[%+=/%*%^%%<>!~|&,:]+", type = "operator"}, + {pattern = "%f[%S]%-[%w%-_]+", type = "function"}, + {pattern = "[%u][%a]+[%-][%u][%a]+", type = "function"}, + {pattern = "${.*}", type = "symbol"}, + {pattern = "$[%a_@*][%w_]*", type = "keyword2"}, + {pattern = "$[%$][%a]+", type = "keyword2"}, + {pattern = "[%a_][%w_]*", type = "symbol"} + }, + symbols = { + ["if"] = "keyword", + ["else"] = "keyword", + ["elseif"] = "keyword", + ["switch"] = "keyword", + ["default"] = "keyword", + ["function"] = "keyword", + ["filter"] = "keyword", + ["workflow"] = "keyword", + ["configuration"] = "keyword", + ["class"] = "keyword", + ["enum"] = "keyword", + ["Parameter"] = "keyword", + ["ValidateScript"] = "keyword", + ["CmdletBinding"] = "keyword", + ["try"] = "keyword", + ["catch"] = "keyword", + ["finally"] = "keyword", + ["throw"] = "keyword", + ["while"] = "keyword", + ["for"] = "keyword", + ["do"] = "keyword", + ["until"] = "keyword", + ["break"] = "keyword", + ["continue"] = "keyword", + ["foreach"] = "keyword", + ["in"] = "keyword", + ["return"] = "keyword", + ["where"] = "function", + ["select"] = "function", + ["filter"] = "keyword", + ["in"] = "keyword", + ["trap"] = "keyword", + ["param"] = "keyword", + ["data"] = "keyword", + ["dynamicparam"] = "keyword", + ["begin"] = "function", + ["process"] = "function", + ["end"] = "function", + ["exit"] = "function", + ["inlinescript"] = "function", + ["parallel"] = "function", + ["sequence"] = "function", + ["true"] = "literal", + ["false"] = "literal", + ["TODO"] = "comment", + ["FIXME"] = "comment", + ["XXX"] = "comment", + ["TBD"] = "comment", + ["HACK"] = "comment", + ["NOTE"] = "comment" + } } diff --git a/plugins/language_psql.lua b/plugins/language_psql.lua index 80bc4cd..b891dae 100644 --- a/plugins/language_psql.lua +++ b/plugins/language_psql.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" -- In sql symbols can be lower case and upper case diff --git a/plugins/language_rescript.lua b/plugins/language_rescript.lua index 9007dc1..3d3bca0 100644 --- a/plugins/language_rescript.lua +++ b/plugins/language_rescript.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_rivet.lua b/plugins/language_rivet.lua index 61b11bc..214d76b 100644 --- a/plugins/language_rivet.lua +++ b/plugins/language_rivet.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Syntax highlighting for the Rivet programming language. -- by StunxFS :) @@ -24,7 +24,7 @@ syntax.add { {pattern = "-?%.?%d+", type = "number"}, {pattern = "[%+%-=/%*%^%%<>!~|&%.%?]", type = "operator"}, -- Uppercase constants of at least 2 chars in length - { + { pattern = "_?%u[%u_][%u%d_]*%f[%s%+%*%-%.%)%]}%?%^%%=/<>~|&;:,!]", type = "number" }, @@ -80,7 +80,7 @@ syntax.add { ["is"] = "keyword", ["in"] = "keyword", ["as"] = "keyword", - + -- types ["no_return"] = "keyword2", ["bool"] = "keyword2", diff --git a/plugins/language_ruby.lua b/plugins/language_ruby.lua index 85717fe..c46d558 100644 --- a/plugins/language_ruby.lua +++ b/plugins/language_ruby.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -23,7 +23,6 @@ syntax.add { }, symbols = { ["nil"] = "literal", - ["end"] = "literal", ["true"] = "literal", ["false"] = "literal", ["private"] = "keyword", @@ -63,7 +62,6 @@ syntax.add { ["self"] = "keyword", ["super"] = "keyword", ["then"] = "keyword", - ["true"] = "keyword", ["undef"] = "keyword", ["unless"] = "keyword", ["until"] = "keyword", diff --git a/plugins/language_rust.lua b/plugins/language_rust.lua index f20d35e..848e8b1 100644 --- a/plugins/language_rust.lua +++ b/plugins/language_rust.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -23,68 +23,66 @@ syntax.add { { pattern = "[%a_][%w_]*", type = "symbol" }, }, symbols = { - ["as"] = "keyword", - ["async"] = "keyword", - ["await"] = "keyword", - ["break"] = "keyword", - ["const"] = "keyword", - ["continue"] = "keyword", - ["crate"] = "keyword", - ["dyn"] = "keyword", - ["else"] = "keyword", - ["enum"] = "keyword", - ["extern"] = "keyword", - ["false"] = "keyword", - ["fn"] = "keyword", - ["for"] = "keyword", - ["if"] = "keyword", - ["impl"] = "keyword", - ["in"] = "keyword", - ["let"] = "keyword", - ["loop"] = "keyword", - ["match"] = "keyword", - ["mod"] = "keyword", - ["move"] = "keyword", - ["mut"] = "keyword", - ["pub"] = "keyword", - ["ref"] = "keyword", - ["return"] = "keyword", - ["Self"] = "keyword", - ["self"] = "keyword", - ["static"] = "keyword", - ["struct"] = "keyword", - ["super"] = "keyword", - ["trait"] = "keyword", - ["true"] = "keyword", - ["type"] = "keyword", - ["unsafe"] = "keyword", - ["use"] = "keyword", - ["where"] = "keyword", - ["while"] = "keyword", - ["i32"] = "keyword2", - ["i64"] = "keyword2", - ["i128"] = "keyword2", - ["i16"] = "keyword2", - ["i8"] = "keyword2", - ["u8"] = "keyword2", - ["u16"] = "keyword2", - ["u32"] = "keyword2", - ["u64"] = "keyword2", - ["usize"] = "keyword2", - ["isize"] = "keyword2", - ["f32"] = "keyword2", - ["f64"] = "keyword2", - ["f128"] = "keyword2", - ["String"] = "keyword2", - ["char"] = "keyword2", - ["str"] = "keyword2", - ["bool"] = "keyword2", - ["true"] = "literal", - ["false"] = "literal", - ["None"] = "literal", - ["Some"] = "literal", - ["Option"] = "literal", - ["Result"] = "literal", + ["as"] = "keyword", + ["async"] = "keyword", + ["await"] = "keyword", + ["break"] = "keyword", + ["const"] = "keyword", + ["continue"] = "keyword", + ["crate"] = "keyword", + ["dyn"] = "keyword", + ["else"] = "keyword", + ["enum"] = "keyword", + ["extern"] = "keyword", + ["fn"] = "keyword", + ["for"] = "keyword", + ["if"] = "keyword", + ["impl"] = "keyword", + ["in"] = "keyword", + ["let"] = "keyword", + ["loop"] = "keyword", + ["match"] = "keyword", + ["mod"] = "keyword", + ["move"] = "keyword", + ["mut"] = "keyword", + ["pub"] = "keyword", + ["ref"] = "keyword", + ["return"] = "keyword", + ["Self"] = "keyword", + ["self"] = "keyword", + ["static"] = "keyword", + ["struct"] = "keyword", + ["super"] = "keyword", + ["trait"] = "keyword", + ["type"] = "keyword", + ["unsafe"] = "keyword", + ["use"] = "keyword", + ["where"] = "keyword", + ["while"] = "keyword", + ["i32"] = "keyword2", + ["i64"] = "keyword2", + ["i128"] = "keyword2", + ["i16"] = "keyword2", + ["i8"] = "keyword2", + ["u8"] = "keyword2", + ["u16"] = "keyword2", + ["u32"] = "keyword2", + ["u64"] = "keyword2", + ["usize"] = "keyword2", + ["isize"] = "keyword2", + ["f32"] = "keyword2", + ["f64"] = "keyword2", + ["f128"] = "keyword2", + ["String"] = "keyword2", + ["char"] = "keyword2", + ["str"] = "keyword2", + ["bool"] = "keyword2", + ["true"] = "literal", + ["false"] = "literal", + ["None"] = "literal", + ["Some"] = "literal", + ["Option"] = "literal", + ["Result"] = "literal", }, } diff --git a/plugins/language_sass.lua b/plugins/language_sass.lua index 723d975..f440927 100644 --- a/plugins/language_sass.lua +++ b/plugins/language_sass.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_scala.lua b/plugins/language_scala.lua index fddadc7..6c5d7a7 100644 --- a/plugins/language_scala.lua +++ b/plugins/language_scala.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -19,62 +19,62 @@ syntax.add { { pattern = "[%a_][%w_]*", type = "symbol" }, }, symbols = { - ["abstract"] = "keyword", - ["case"] = "keyword", - ["catch"] = "keyword", - ["class"] = "keyword", - ["finally"] = "keyword", - ["final"] = "keyword", - ["do"] = "keyword", - ["extends"] = "keyword", - ["forSome"] = "keyword", - ["implicit"] = "keyword", - ["lazy"] = "keyword", - ["match"] = "keyword", - ["new"] = "keyword", - ["override"] = "keyword", - ["package"] = "keyword", - ["throw"] = "keyword", - ["trait"] = "keyword", - ["type"] = "keyword", - ["var"] = "keyword", - ["val"] = "keyword", + ["abstract"] = "keyword", + ["case"] = "keyword", + ["catch"] = "keyword", + ["class"] = "keyword", + ["finally"] = "keyword", + ["final"] = "keyword", + ["do"] = "keyword", + ["extends"] = "keyword", + ["forSome"] = "keyword", + ["implicit"] = "keyword", + ["lazy"] = "keyword", + ["match"] = "keyword", + ["new"] = "keyword", + ["override"] = "keyword", + ["package"] = "keyword", + ["throw"] = "keyword", + ["trait"] = "keyword", + ["type"] = "keyword", + ["var"] = "keyword", + ["val"] = "keyword", ["println"] = "keyword", - ["return"] = "keyword", - ["for"] = "keyword", - ["Try"] = "keyword", - ["def"] = "keyword", - ["while"] = "keyword", - ["with"] = "keyword", - ["if"] = "keyword", - ["else"] = "keyword", - ["import"] = "keyword", - ["object"] = "keyword", - ["yield"] = "keyword", - - ["private"] = "keyword2", - ["protected"] = "keyword2", - ["sealed"] = "keyword2", - ["super"] = "keyword2", - ["this"] = "keyword2", - ["Byte"] = "keyword2", - ["Short"] = "keyword2", - ["Int"] = "keyword2", - ["Long"] = "keyword2", - ["Float"] = "keyword2", - ["Double"] = "keyword2", - ["Char"] = "keyword2", - ["String"] = "keyword2", - ["List"] = "keyword2", - ["Array"] = "keyword2", - ["Boolean"] = "keyword2", - - ["Null"] = "literal", - ["Any"] = "literal", - ["AnyRef"] = "literal", - ["Nothing"] = "literal", - ["Unit"] = "literal", - ["true"] = "literal", - ["false"] = "literal", + ["return"] = "keyword", + ["for"] = "keyword", + ["Try"] = "keyword", + ["def"] = "keyword", + ["while"] = "keyword", + ["with"] = "keyword", + ["if"] = "keyword", + ["else"] = "keyword", + ["import"] = "keyword", + ["object"] = "keyword", + ["yield"] = "keyword", + + ["private"] = "keyword2", + ["protected"] = "keyword2", + ["sealed"] = "keyword2", + ["super"] = "keyword2", + ["this"] = "keyword2", + ["Byte"] = "keyword2", + ["Short"] = "keyword2", + ["Int"] = "keyword2", + ["Long"] = "keyword2", + ["Float"] = "keyword2", + ["Double"] = "keyword2", + ["Char"] = "keyword2", + ["String"] = "keyword2", + ["List"] = "keyword2", + ["Array"] = "keyword2", + ["Boolean"] = "keyword2", + + ["Null"] = "literal", + ["Any"] = "literal", + ["AnyRef"] = "literal", + ["Nothing"] = "literal", + ["Unit"] = "literal", + ["true"] = "literal", + ["false"] = "literal", } } diff --git a/plugins/language_sh.lua b/plugins/language_sh.lua index fdfe867..5d7b987 100644 --- a/plugins/language_sh.lua +++ b/plugins/language_sh.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_ssh_config.lua b/plugins/language_ssh_config.lua index c105082..894ce21 100644 --- a/plugins/language_ssh_config.lua +++ b/plugins/language_ssh_config.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_tcl.lua b/plugins/language_tcl.lua index 1fb672e..7a6e13f 100644 --- a/plugins/language_tcl.lua +++ b/plugins/language_tcl.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_teal.lua b/plugins/language_teal.lua index a974fdd..0b0e35d 100644 --- a/plugins/language_teal.lua +++ b/plugins/language_teal.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_tex.lua b/plugins/language_tex.lua index 264c5ca..f8e69e2 100644 --- a/plugins/language_tex.lua +++ b/plugins/language_tex.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_toml.lua b/plugins/language_toml.lua index 2b7fd01..02e5b58 100644 --- a/plugins/language_toml.lua +++ b/plugins/language_toml.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local syntax = require "core.syntax" diff --git a/plugins/language_ts.lua b/plugins/language_ts.lua index 13c6ac2..f28261d 100644 --- a/plugins/language_ts.lua +++ b/plugins/language_ts.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- copied from language_js, but added regex highlighting back local syntax = require "core.syntax" diff --git a/plugins/language_tsx.lua b/plugins/language_tsx.lua index 473f808..beedbc6 100644 --- a/plugins/language_tsx.lua +++ b/plugins/language_tsx.lua @@ -1,5 +1,6 @@ --- mod-version:2 -- lite-xl 2.0 --- Almost identical to JS, with the exception that / shouldn't denote a regex. Current JS syntax highlighter will highlight half the document due to closing tags. +-- mod-version:3 +-- Almost identical to JS, with the exception that / shouldn't denote a regex. +-- Current JS syntax highlighter will highlight half the document due to closing tags. local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_v.lua b/plugins/language_v.lua index 4afd3fd..d5b9764 100644 --- a/plugins/language_v.lua +++ b/plugins/language_v.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/language_wren.lua b/plugins/language_wren.lua index 9bd6c82..2022dbf 100644 --- a/plugins/language_wren.lua +++ b/plugins/language_wren.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { @@ -22,19 +22,15 @@ syntax.add { ["class"] = "keyword", ["construct"] = "keyword", ["else"] = "keyword", - ["false"] = "keyword", ["for"] = "keyword", ["foreign"] = "keyword", ["if"] = "keyword", ["import"] = "keyword", ["in"] = "keyword", ["is"] = "keyword", - ["null"] = "keyword", ["return"] = "keyword", ["static"] = "keyword", ["super"] = "keyword", - ["this"] = "keyword", - ["true"] = "keyword", ["var"] = "keyword", ["while"] = "keyword", ["this"] = "keyword2", diff --git a/plugins/language_yaml.lua b/plugins/language_yaml.lua index a83e89f..8b7f634 100644 --- a/plugins/language_yaml.lua +++ b/plugins/language_yaml.lua @@ -1,53 +1,149 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" +local yaml_bracket_list = { + patterns = { + -- comments + { pattern = { "#", "\n"}, type = "comment" }, + -- strings + { pattern = { '"', '"', '\\' }, type = "string" }, + { pattern = { "'", "'", '\\' }, type = "string" }, + -- keys + { + pattern = "[%w%d]+%g+()%s*():()%s", + type = { "keyword2", "normal", "operator", "normal" } + }, + -- variables + { pattern = "%$%a%w+", type = "keyword" }, + { pattern = "%$%{%{.-%}%}", type = "keyword" }, + -- numeric place holders + { pattern = "%-?%.inf", type = "number" }, + { pattern = "%.NaN", type = "number" }, + -- numbers + { pattern = "[%+%-]?0%d+", type = "number" }, + { pattern = "[%+%-]?0x%x+", type = "number" }, + { pattern = "[%+%-]?%d+[,%.eE:%+%d]*%d+", type = "number" }, + { pattern = "[%+%-]?%d+", type = "number" }, + -- others + { pattern = ",", type = "operator" }, + { pattern = "%w+", type = "string" }, + { + pattern = "[_%(%)%*@~`!%%%^&=%+%-\\;%.><%?/%s]+", + type = "string" + } + }, + symbols = {} +} + syntax.add { name = "YAML", files = { "%.yml$", "%.yaml$" }, comment = "#", + space_handling = false, patterns = { + --- rules that start with spaces first and those taking precedence + -- parent and child keys + { + pattern = "^[%w%d]+%g+%s*%f[:]", + type = "literal" + }, + { + pattern = "^%s+[%w%d]+%g+%s*%f[:]", + type = "keyword2" + }, + -- bracket lists after key declaration + { + pattern = { ":%s+%[", "%]" }, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { ":%s+{", "}" }, + syntax = yaml_bracket_list, type = "operator" + }, + -- child key + { + pattern = "^%s+()[%w%d]+%g+()%s*():()%s", + type = { "normal", "keyword2", "normal", "operator", "normal" } + }, + -- child list element + { + pattern = "^%s+()%-()%s+()[%w%d]+%g+()%s*():()%s", + type = { "normal", "operator", "normal", "keyword2", "normal", "operator", "normal" } + }, + -- unkeyed bracket lists + { + pattern = { "^%s*%[", "%]" }, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { "^%s*{", "}" }, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { "^%s*%-%s*%[", "%]" }, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { "^%s*%-%s*{", "}" }, + syntax = yaml_bracket_list, type = "operator" + }, + -- rule to optimize space handling + { pattern = "%s+", type = "normal" }, + --- all the other rules + -- comments { pattern = { "#", "\n"}, type = "comment" }, + -- strings { pattern = { '"', '"', '\\' }, type = "string" }, { pattern = { "'", "'", '\\' }, type = "string" }, + -- extra bracket lists rules on explicit type + { + pattern = { "!!%w+%s+%[", "%]"}, + syntax = yaml_bracket_list, type = "operator" + }, + { + pattern = { "!!%w+%s+{", "}"}, + syntax = yaml_bracket_list, type = "operator" + }, + -- numeric place holders { pattern = "%-?%.inf", type = "number" }, { pattern = "%.NaN", type = "number" }, + -- parent list element + { + pattern = "^%-()%s+()[%w%d]+%g+()%s*():()%s", + type = { "operator", "normal", "keyword2", "normal", "operator", "normal" } + }, + -- key label { pattern = "%&()%g+", type = { "keyword", "literal" } }, - { pattern = "!%g+", type = "keyword" }, + -- key elements expansion { pattern = "<<", type = "literal" }, { - pattern = "[%s]%*()[%w%d_]+", - type = { "keyword", "keyword2" } - }, - { pattern = "%*()[%w%d_]+", type = { "keyword", "literal" } }, + -- explicit data types + { pattern = "!!%g+", type = "keyword" }, + -- parent key { - pattern = "[%[%{]()%s*[%w%d]+%g+%s*():%s", - type = { "operator", "keyword2", "operator" } + pattern = "^[%w%d]+%g+()%s*():()%s", + type = { "literal", "normal", "operator", "normal" } }, - { - pattern = "[%s][%w%d]+%g+%s*():%s", - type = { "keyword2", "operator" } - }, - { - pattern = "[%w%d]+%g+%s*():%s", - type = { "literal", "operator" } - }, - { pattern = "0%d+", type = "number" }, - { pattern = "0x%x+", type = "number" }, + -- variables + { pattern = "%$%a%w+", type = "keyword" }, + { pattern = "%$%{%{.-%}%}", type = "keyword" }, + -- numbers + { pattern = "[%+%-]?0%d+", type = "number" }, + { pattern = "[%+%-]?0x%x+", type = "number" }, { pattern = "[%+%-]?%d+[,%.eE:%+%d]*%d+", type = "number" }, + { pattern = "[%+%-]?%d+", type = "number" }, + -- special operators { pattern = "[%*%|%!>%%]", type = "keyword" }, - { pattern = "[%-:%?%*%{%}%[%]]", type = "operator" }, - -- Everything else for keywords to work - { - pattern = "[%d%a_][%g_]*()[%]%},]", - type = { "string", "operator" } - }, + { pattern = "[%-%$:%?]+", type = "operator" }, + -- Everything else as a string { pattern = "[%d%a_][%g_]*", type = "string" }, + { pattern = "%p+", type = "string" } }, symbols = { ["true"] = "number", diff --git a/plugins/language_zig.lua b/plugins/language_zig.lua index 192b114..f2c32d5 100644 --- a/plugins/language_zig.lua +++ b/plugins/language_zig.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local syntax = require "core.syntax" syntax.add { diff --git a/plugins/lfautoinsert.lua b/plugins/lfautoinsert.lua index a156964..ae3da75 100644 --- a/plugins/lfautoinsert.lua +++ b/plugins/lfautoinsert.lua @@ -1,11 +1,11 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local common = require "core.common" local config = require "core.config" local keymap = require "core.keymap" -config.plugins.lfautoinsert = { map = { +config.plugins.lfautoinsert = common.merge({ map = { ["{%s*\n"] = "}", ["%(%s*\n"] = ")", ["%f[[]%[%s*\n"] = "]", @@ -37,7 +37,7 @@ config.plugins.lfautoinsert = { map = { ["%[%[%s*\n"] = "]]" } }, -} } +} }, config.plugins.lfautoinsert) local function get_autoinsert_map(filename) local map = {} @@ -64,7 +64,7 @@ local function indent_size(doc, line) return e - s end -command.add("core.docview", { +command.add("core.docview!", { ["autoinsert:newline"] = function() command.perform("doc:newline") @@ -97,7 +97,7 @@ command.add("core.docview", { }) keymap.add { - ["return"] = { "command:submit", "autoinsert:newline" } + ["return"] = { "autoinsert:newline" } } return { diff --git a/plugins/linecopypaste.lua b/plugins/linecopypaste.lua deleted file mode 100644 index ea2a84c..0000000 --- a/plugins/linecopypaste.lua +++ /dev/null @@ -1,50 +0,0 @@ --- mod-version:2 -- lite-xl 2.0
-local core = require "core"
-local command = require "core.command"
-
-local function doc()
- return core.active_view.doc
-end
-
-local line_in_clipboard = false
-
-local doc_copy = command.map["doc:copy"].perform
-command.map["doc:copy"].perform = function()
- if doc():has_selection() then
- doc_copy()
- line_in_clipboard = false
- else
- local line = doc():get_selection()
- system.set_clipboard(doc().lines[line])
- line_in_clipboard = true
- end
-end
-
-local doc_cut = command.map["doc:cut"].perform
-command.map["doc:cut"].perform = function()
- if doc():has_selection() then
- doc_cut()
- line_in_clipboard = false
- else
- local line = doc():get_selection()
- system.set_clipboard(doc().lines[line])
- if line < #(doc().lines) then
- doc():remove(line, 1, line+1, 1)
- else -- last line in file
- doc():remove(line, 1, line, #(doc().lines[line]))
- end
- doc():set_selection(line, 1)
- line_in_clipboard = true
- end
-end
-
-local doc_paste = command.map["doc:paste"].perform
-command.map["doc:paste"].perform = function()
- if line_in_clipboard == false then
- doc_paste()
- else
- local line, col = doc():get_selection()
- doc():insert(line, 1, system.get_clipboard():gsub("\r", ""))
- doc():set_selection(line+1, col)
- end
-end
diff --git a/plugins/linenumbers.lua b/plugins/linenumbers.lua index 1b0bdc6..c075776 100644 --- a/plugins/linenumbers.lua +++ b/plugins/linenumbers.lua @@ -1,85 +1,116 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" local common = require "core.common" local command = require "core.command" -local draw = DocView.draw_line_gutter -local get_width = DocView.get_gutter_width +config.plugins.linenumbers = common.merge({ + show = true, + relative = false, + -- The config specification used by the settings gui + config_spec = { + name = "Line Numbers", + { + label = "Show Numbers", + description = "Display or hide the line numbers.", + path = "show", + type = "toggle", + default = true + }, + { + label = "Relative Line Numbers", + description = "Display relative line numbers starting from active line.", + path = "relative", + type = "toggle", + default = false + } + } +}, config.plugins.linenumbers) -function DocView:draw_line_gutter(idx, x, y, width) - if not config.line_numbers and not config.relative_line_numbers then - return - end +local draw_line_gutter = DocView.draw_line_gutter +local get_gutter_width = DocView.get_gutter_width - if config.relative_line_numbers then +function DocView:draw_line_gutter(line, x, y, width) + local lh = self:get_line_height() + if not config.plugins.linenumbers.show then + return lh + end + if config.plugins.linenumbers.relative then local color = style.line_number - local local_idx = idx - local align = "right" + local local_idx = line - local l1 = self.doc:get_selection(false) - if idx == l1 then - color = style.line_number2 - if config.line_numbers then - align = "center" - else - local_idx = 0 + for _, line1, _, line2 in self.doc:get_selections(true) do + if line >= line1 and line <= line2 then + color = style.line_number2 + break end - else - local_idx = math.abs(idx - l1) end - -- Fix for old version (<=1.16) - if width == nil then - local gpad = style.padding.x * 2 - local gw = self:get_font():get_width(#self.doc.lines) + gpad - width = gpad and gw - gpad or gw + local l1 = self.doc:get_selection(false) + if line == l1 then + color = style.line_number2 + local_idx = 0 + else + local_idx = math.abs(line - l1) end common.draw_text( self:get_font(), - color, local_idx, align, + color, local_idx, "right", x + style.padding.x, - y + self:get_line_text_y_offset(), - width, self:get_line_height() + y, + width, lh ) else - draw(self, idx, x, y, width) + return draw_line_gutter(self, line, x, y, width) end + return lh end function DocView:get_gutter_width(...) - if not config.line_numbers and not config.relative_line_numbers then - return style.padding.x + if + not config.plugins.linenumbers.show + then + local width = get_gutter_width(self, ...) + + local correct_width = self:get_font():get_width(#self.doc.lines) + + (style.padding.x * 2) + + -- compatibility with center doc + if width <= correct_width then + width = style.padding.x + end + + return width, 0 else - return get_width(self, ...) + return get_gutter_width(self, ...) end end command.add(nil, { ["line-numbers:toggle"] = function() - config.line_numbers = not config.line_numbers + config.plugins.linenumbers.show = not config.plugins.linenumbers.show end, ["line-numbers:disable"] = function() - config.line_numbers = false + config.plugins.linenumbers.show = false end, ["line-numbers:enable"] = function() - config.line_numbers = true + config.plugins.linenumbers.show = true end, ["relative-line-numbers:toggle"] = function() - config.relative_line_numbers = not config.relative_line_numbers + config.plugins.linenumbers.relative = not config.plugins.linenumbers.relative end, ["relative-line-numbers:enable"] = function() - config.relative_line_numbers = true + config.plugins.linenumbers.relative = true end, ["relative-line-numbers:disable"] = function() - config.relative_line_numbers = false + config.plugins.linenumbers.relative = false end }) diff --git a/plugins/macmodkeys.lua b/plugins/macmodkeys.lua index d8c0d05..e4656b9 100644 --- a/plugins/macmodkeys.lua +++ b/plugins/macmodkeys.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local keymap = require "core.keymap" local on_key_pressed = keymap.on_key_pressed diff --git a/plugins/markers.lua b/plugins/markers.lua index ad89fad..e2ec68e 100644 --- a/plugins/markers.lua +++ b/plugins/markers.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0
+-- mod-version:3
-- Markers plugin for lite text editor
-- original implementation by Petri Häkkinen
@@ -52,12 +52,12 @@ end local draw_line_gutter = DocView.draw_line_gutter
-function DocView:draw_line_gutter(idx, x, y, width)
- if cache[self.doc] and cache[self.doc][idx] then
+function DocView:draw_line_gutter(line, x, y, width)
+ if cache[self.doc] and cache[self.doc][line] then
local h = self:get_line_height()
renderer.draw_rect(x, y, style.padding.x * 0.4, h, style.selection)
end
- draw_line_gutter(self, idx, x, y, width)
+ return draw_line_gutter(self, line, x, y, width)
end
diff --git a/plugins/memoryusage.lua b/plugins/memoryusage.lua index fcdcc29..290b4e1 100644 --- a/plugins/memoryusage.lua +++ b/plugins/memoryusage.lua @@ -1,19 +1,49 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- original implementation by AqilCont +local core = require "core" +local config = require "core.config" +local common = require "core.common" local style = require "core.style" local StatusView = require "core.statusview" -local get_items = StatusView.get_items - -function StatusView:get_items() - local left, right = get_items(self) - local t = { - style.text, (math.floor(collectgarbage("count") / 10.24) / 100) .. " MB", - style.dim, self.separator2, +config.plugins.memoryusage = common.merge({ + enabled = true, + -- The config specification used by the settings gui + config_spec = { + name = "Memory Usage", + { + label = "Enabled", + description = "Show or hide the lua memory usage from the status bar.", + path = "enabled", + type = "toggle", + default = true, + on_apply = function(enabled) + core.add_thread(function() + if enabled then + core.status_view:get_item("status:memory-usage"):show() + else + core.status_view:get_item("status:memory-usage"):hide() + end + end) + end + } } - for i, item in ipairs(t) do - table.insert(right, i, item) - end - return left, right -end +}, config.plugins.memoryusage) + +core.status_view:add_item({ + name = "status:memory-usage", + alignment = StatusView.Item.RIGHT, + get_item = function() + return { + style.text, + string.format( + "%.2f MB", + (math.floor(collectgarbage("count") / 10.24) / 100) + ) + } + end, + position = 1, + tooltip = "lua memory usage", + separator = core.status_view.separator2 +}) diff --git a/plugins/minimap.lua b/plugins/minimap.lua index 4bf4fca..c2dd8f0 100644 --- a/plugins/minimap.lua +++ b/plugins/minimap.lua @@ -1,382 +1,666 @@ --- mod-version:2 +-- mod-version:3 local core = require "core" local command = require "core.command" local common = require "core.common" local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" +local Highlighter = require "core.doc.highlighter" local Object = require "core.object" +local Scrollbar = require "core.scrollbar" + +-- Sample configurations: +-- full width: +-- config.plugins.minimap.highlight_width = 100 +-- config.plugins.minimap.gutter_width = 0 +-- left side: +-- config.plugins.minimap.highlight_align = 'left' +-- config.plugins.minimap.highlight_width = 3 +-- config.plugins.minimap.gutter_width = 4 +-- right side: +-- config.plugins.minimap.highlight_align = 'right' +-- config.plugins.minimap.highlight_width = 5 +-- config.plugins.minimap.gutter_width = 0 -- General plugin settings -config.plugins.minimap = { - enabled = true, - width = 100, - instant_scroll = false, - syntax_highlight = true, - scale = 1, - -- how many spaces one tab is equivalent to - tab_width = 4, - draw_background = true, - - -- you can override these colors - selection_color = nil, - caret_color = nil, - - -- If other plugins provide per-line highlights, - -- this controls the placement. (e.g. gitdiff_highlight) - highlight_align = 'left', - highlight_width = 3, - gutter_width = 5, - -- try these values: - -- full width: - -- config.plugins.minimap.highlight_width = 100 - -- config.plugins.minimap.gutter_width = 0 - -- left side: - -- config.plugins.minimap.highlight_align = 'left' - -- config.plugins.minimap.highlight_width = 3 - -- config.plugins.minimap.gutter_width = 4 - -- right side: - -- config.plugins.minimap.highlight_align = 'right' - -- config.plugins.minimap.highlight_width = 5 - -- config.plugins.minimap.gutter_width = 0 +config.plugins.minimap = common.merge({ + enabled = true, + width = 100, + instant_scroll = false, + syntax_highlight = true, + scale = 1, + -- number of spaces needed to split a token + spaces_to_split = 2, + -- hide on small docs (can be true, false or min number of lines) + avoid_small_docs = false, + -- how many spaces one tab is equivalent to + tab_width = 4, + draw_background = true, + -- you can override these colors + selection_color = nil, + caret_color = nil, + -- If other plugins provide per-line highlights, + -- this controls the placement. (e.g. gitdiff_highlight) + highlight_align = 'left', + highlight_width = 3, + gutter_width = 5, + -- The config specification used by the settings gui + config_spec = { + name = "Mini Map", + { + label = "Enabled", + description = "Activate the minimap by default.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Width", + description = "Width of the minimap in pixels.", + path = "width", + type = "number", + default = 100, + min = 50, + max = 1000 + }, + { + label = "Instant Scroll", + description = "When enabled disables the scrolling animation.", + path = "instant_scroll", + type = "toggle", + default = false + }, + { + label = "Syntax Highlighting", + description = "Disable to improve performance.", + path = "syntax_highlight", + type = "toggle", + default = true + }, + { + label = "Scale", + description = "Size of the minimap using a scaling factor.", + path = "scale", + type = "number", + default = 1, + min = 0.5, + max = 10, + step = 0.1 + }, + { + label = "Spaces to split", + description = "Number of spaces needed to split a token.", + path = "spaces_to_split", + type = "number", + default = 2, + min = 1 + }, + { + label = "Hide for small Docs", + description = "Hide the minimap when a Doc is small enough.", + path = "avoid_small_docs", + type = "toggle", + default = false + }, + { + label = "Small Docs definition", + description = "Size of a Doc to be considered small. Use 0 to automatically decide.", + path = "avoid_small_docs_len", + type = "number", + default = 0, + min = 0, + on_apply = function(value) + if value == 0 then + config.plugins.minimap.avoid_small_docs = true + else + config.plugins.minimap.avoid_small_docs = value + end + end + }, + { + label = "Tabs Width", + description = "The amount of spaces that represent a tab.", + path = "tab_width", + type = "number", + default = 4, + min = 1, + max = 8 + }, + { + label = "Draw Background", + description = "When disabled makes the minimap transparent.", + path = "draw_background", + type = "toggle", + default = true + }, + { + label = "Selection Color", + description = "Background color of selected text in html notation eg: #FFFFFF. Leave empty to use default.", + path = "selection_color_html", + type = "string", + on_apply = function(value) + if value and value:match("#%x%x%x%x%x%x") then + config.plugins.minimap.selection_color = { common.color(value) } + else + config.plugins.minimap.selection_color = nil + end + end + }, + { + label = "Caret Color", + description = "Background color of active line in html notation eg: #FFFFFF. Leave empty to use default.", + path = "caret_color_html", + type = "string", + on_apply = function(value) + if value and value:match("#%x%x%x%x%x%x") then + config.plugins.minimap.caret_color = { common.color(value) } + else + config.plugins.minimap.caret_color = nil + end + end + }, + { + label = "Highlight Alignment", + path = "highlight_align", + type = "selection", + default = "left", + values = { + {"Left", "left"}, + {"Right", "right"} + } + }, + { + label = "Highlight Width", + path = "highlight_width", + type = "number", + default = 3, + min = 0, + max = 50 + }, + { + label = "Gutter Width", + description = "Left padding of the minimap.", + path = "gutter_width", + type = "number", + default = 5, + min = 0, + max = 50 + }, + } +}, config.plugins.minimap) + + +-- contains the settings values that require a cache reset if changed +local cached_settings = { + color_scheme_canary = nil, + syntax_highlight = nil, + spaces_to_split = nil, + scale = nil, + width = nil, } -- Configure size for rendering each char in the minimap -local char_height = 1 * SCALE * config.plugins.minimap.scale -local char_spacing = 0.8 * SCALE * config.plugins.minimap.scale -local line_spacing = 2 * SCALE * config.plugins.minimap.scale +local char_spacing +local char_height +local line_spacing + +-- cache for the location of the rects for each Doc +local highlighter_cache +local function reset_cache() + highlighter_cache = setmetatable({}, { __mode = "k" }) + cached_settings = { + color_scheme_canary = style.syntax["normal"], + syntax_highlight = config.plugins.minimap.syntax_highlight, + spaces_to_split = config.plugins.minimap.spaces_to_split, + scale = config.plugins.minimap.scale, + width = config.plugins.minimap.width, + } + char_spacing = 0.8 * SCALE * config.plugins.minimap.scale + -- keep y aligned to pixels + char_height = math.max(1, math.floor(1 * SCALE * config.plugins.minimap.scale + 0.5)) + line_spacing = math.max(1, math.floor(2 * SCALE * config.plugins.minimap.scale + 0.5)) +end +reset_cache() + + +local function reset_cache_if_needed() + if + cached_settings.color_scheme_canary ~= style.syntax["normal"] + or cached_settings.syntax_highlight ~= config.plugins.minimap.syntax_highlight + or cached_settings.spaces_to_split ~= config.plugins.minimap.spaces_to_split + or cached_settings.scale ~= config.plugins.minimap.scale + or cached_settings.width ~= config.plugins.minimap.width + then + reset_cache() + end +end + + + + +-- Move cache to make space for new lines +local prev_insert_notify = Highlighter.insert_notify +function Highlighter:insert_notify(line, n, ...) + prev_insert_notify(self, line, n, ...) + local blanks = { } + if not highlighter_cache[self] then + highlighter_cache[self] = {} + else + local to = math.min(line + n, #self.doc.lines) + for i=#self.doc.lines+n,to,-1 do + highlighter_cache[self][i] = highlighter_cache[self][i - n] + end + for i=line,to do + highlighter_cache[self][i] = nil + end + end +end + + +-- Close the cache gap created by removed lines +local prev_remove_notify = Highlighter.remove_notify +function Highlighter:remove_notify(line, n, ...) + prev_remove_notify(self, line, n, ...) + if not highlighter_cache[self] then + highlighter_cache[self] = {} + else + local to = math.max(line + n, #self.doc.lines) + for i=line,to do + highlighter_cache[self][i] = highlighter_cache[self][i + n] + end + end +end + + +-- Remove changed lines from the cache +local prev_tokenize_line = Highlighter.tokenize_line +function Highlighter:tokenize_line(idx, state, ...) + local res = prev_tokenize_line(self, idx, state, ...) + if not highlighter_cache[self] then + highlighter_cache[self] = {} + end + highlighter_cache[self][idx] = nil + return res +end + +-- Ask the Highlighter to retokenize the lines we have in cache +local prev_invalidate = Highlighter.invalidate +function Highlighter:invalidate(idx, ...) + local cache = highlighter_cache[self] + if cache then + self.max_wanted_line = math.max(self.max_wanted_line, #cache) + end + return prev_invalidate(self, idx, ...) +end + + +-- Remove cache on Highlighter reset (for example on syntax change) +local prev_soft_reset = Highlighter.soft_reset +function Highlighter:soft_reset(...) + prev_soft_reset(self, ...) + highlighter_cache[self] = {} +end + + +local MiniMap = Scrollbar:extend() -local MiniMap = Object:extend() -function MiniMap:new() +function MiniMap:new(dv) + MiniMap.super.new(self, { direction = "v", alignment = "e" }) + self.dv = dv + self.enabled = nil end + function MiniMap:line_highlight_color(line_index) - -- other plugins can override this, and return a color + -- other plugins can override this, and return a color end -local minimap = MiniMap() -local function show_minimap() - return config.plugins.minimap.enabled - and getmetatable(core.active_view) == DocView - and core.active_view ~= core.command_view - and core.active_view.doc +function MiniMap:is_minimap_enabled() + if self.enabled ~= nil then return self.enabled end + if not config.plugins.minimap.enabled then return false end + if config.plugins.minimap.avoid_small_docs then + local last_line = #self.dv.doc.lines + if type(config.plugins.minimap.avoid_small_docs) == "number" then + return last_line > config.plugins.minimap.avoid_small_docs + else + local docview = self.dv + local _, y = docview:get_line_screen_position(last_line, docview.doc.lines[last_line]) + y = y + docview.scroll.y - docview.position.y + docview:get_line_height() + return y > docview.size.y + end + end + return true +end + + +function MiniMap:get_minimap_dimensions() + local x, y, w, h = self:get_track_rect() + local _, cy, _, cy2 = self.dv:get_content_bounds() + local lh = self.dv:get_line_height() + + local visible_lines_start = math.max(1, math.floor(cy / lh)) + local visible_lines_count = math.max(1, (cy2 - cy) / lh) + local minimap_lines_start = 1 + local minimap_lines_count = math.floor(h / line_spacing) + local line_count = #self.dv.doc.lines + + local is_file_too_large = line_count > 1 and line_count > minimap_lines_count + if is_file_too_large then + local scroll_pos = (visible_lines_start - 1) / + (line_count - visible_lines_count - 1) + scroll_pos = math.min(1.0, scroll_pos) -- 0..1, procent of visual area scrolled + + local thumb_height = visible_lines_count * line_spacing + local scroll_pos_pixels = scroll_pos * (h - thumb_height) + + minimap_lines_start = visible_lines_start - + math.floor(scroll_pos_pixels / line_spacing) + minimap_lines_start = math.max(1, minimap_lines_start) + end + return visible_lines_start, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large end --- Overloaded since the default implementation adds a extra x3 size of hotspot for the mouse to hit the scrollbar. -local prev_scrollbar_overlaps_point = DocView.scrollbar_overlaps_point -DocView.scrollbar_overlaps_point = function(self, x, y) - if not show_minimap() then - return prev_scrollbar_overlaps_point(self, x, y) - end - local sx, sy, sw, sh = self:get_scrollbar_rect() - return x >= sx and x < sx + sw and y >= sy and y < sy + sh +function MiniMap:_get_track_rect_normal() + if not self:is_minimap_enabled() then return MiniMap.super._get_track_rect_normal(self) end + return self.dv.size.x + self.dv.position.x - config.plugins.minimap.width, self.dv.position.y, config.plugins.minimap.width, self.dv.size.y end --- Helper function to determine if current file is too large to be shown fully inside the minimap area. -local function is_file_too_large(self) - local line_count = #self.doc.lines - local _, _, _, sh = self:get_scrollbar_rect() - -- check if line count is too large to fit inside the minimap area - local max_minmap_lines = math.floor(sh / line_spacing) - return line_count > 1 and line_count > max_minmap_lines +function MiniMap:get_active_margin() if self:is_minimap_enabled() then return 0 else return MiniMap.super.get_active_margin(self) end end + + +function MiniMap:_get_thumb_rect_normal() + if not self:is_minimap_enabled() then return MiniMap.super._get_thumb_rect_normal(self) end + local visible_lines_start, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() + local visible_y = self.dv.position.y + (visible_lines_start - 1) * line_spacing + if is_file_too_large then + local line_count = #self.dv.doc.lines + local scroll_pos = (visible_lines_start - 1) / + (line_count - visible_lines_count - 1) + scroll_pos = math.min(1.0, scroll_pos) -- 0..1, procent of visual area scrolled + + local thumb_height = visible_lines_count * line_spacing + local scroll_pos_pixels = scroll_pos * (self.dv.size.y - thumb_height) + visible_y = self.dv.position.y + scroll_pos_pixels + end + return self.dv.size.x + self.dv.position.x - config.plugins.minimap.width, visible_y, config.plugins.minimap.width, visible_lines_count * line_spacing end --- Overloaded with an extra check if the user clicked inside the minimap to automatically scroll to that line. -local prev_on_mouse_pressed = DocView.on_mouse_pressed -DocView.on_mouse_pressed = function(self, button, x, y, clicks) - if not show_minimap() then - return prev_on_mouse_pressed(self, button, x, y, clicks) - end - - -- check if user clicked in the minimap area and jump directly to that line - -- unless they are actually trying to perform a drag - local minimap_hit = self:scrollbar_overlaps_point(x, y) - if minimap_hit then - local line_count = #self.doc.lines - local minimap_height = line_count * line_spacing - - -- check if line count is too large to fit inside the minimap area - local is_too_large = is_file_too_large(self) - if is_too_large then - local _, _, _, sh = self:get_scrollbar_rect() - minimap_height = sh - end - - -- calc which line to jump to - local dy = y - self.position.y - local jump_to_line = math.floor((dy / minimap_height) * line_count) + 1 - - local _, cy, _, cy2 = self:get_content_bounds() - local lh = self:get_line_height() - local visible_lines_count = math.max(1, (cy2 - cy) / lh) - local visible_lines_start = math.max(1, math.floor(cy / lh)) - - -- calc if user hit the currently visible area - local hit_visible_area = true - if is_too_large then - - local visible_height = visible_lines_count * line_spacing - local scroll_pos = (visible_lines_start - 1) / - (line_count - visible_lines_count - 1) - scroll_pos = math.min(1.0, scroll_pos) -- 0..1 - local visible_y = self.position.y + scroll_pos * - (minimap_height - visible_height) - - local t = (line_count - visible_lines_start) / visible_lines_count - if t <= 1 then visible_y = visible_y + visible_height * (1.0 - t) end - - if y < visible_y or y > visible_y + visible_height then - hit_visible_area = false - end - else - - -- If the click is on the currently visible line numbers, - -- ignore it since then they probably want to initiate a drag instead. - if jump_to_line < visible_lines_start or jump_to_line > visible_lines_start + - visible_lines_count then hit_visible_area = false end - end - - -- if user didn't click on the visible area (ie not dragging), scroll accordingly - if not hit_visible_area then - self:scroll_to_line(jump_to_line, false, config.plugins.minimap.instant_scroll) - end - - end - - return prev_on_mouse_pressed(self, button, x, y, clicks) + +function MiniMap:on_mouse_pressed(button, x, y, clicks) + local percent = MiniMap.super.on_mouse_pressed(self, button, x, y, clicks) + if not self:is_minimap_enabled() or not percent then return percent end + local _, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() + local _, _, w, h = self:get_track_rect() + local tx, ty, tw, th = self:get_thumb_rect() + if y >= ty and y < ty + th then self.drag_start_offset = (y - ty) - th / 2 return self.percent end + self.drag_start_offset = 0 + self.hovering.thumb = x >= tx and x < tx + tw and y >= ty and y < ty + th + self.dragging = self.hovering.thumb + local lh = self.dv:get_line_height() + percent = math.max(0.0, math.min((y - self.dv.position.y) / h, 1.0)) + return (((percent * minimap_lines_count) + minimap_lines_start) * lh / self.dv:get_scrollable_size()) - (visible_lines_count / #self.dv.doc.lines / 2) end --- Overloaded with pretty much the same logic as original DocView implementation, --- with the exception of the dragging scrollbar delta. We want it to behave a bit snappier --- since the "scrollbar" essentially represents the lines visible in the content view. -local prev_on_mouse_moved = DocView.on_mouse_moved -DocView.on_mouse_moved = function(self, x, y, dx, dy) - if not show_minimap() then - return prev_on_mouse_moved(self, x, y, dx, dy) - end - - if self.dragging_scrollbar then - local line_count = #self.doc.lines - local lh = self:get_line_height() - local delta = lh / line_spacing * dy - - if is_file_too_large(self) then - local _, sy, _, sh = self:get_scrollbar_rect() - delta = (line_count * lh) / sh * dy - end - - self.scroll.to.y = self.scroll.to.y + delta - end - - -- we need to "hide" that the scrollbar is dragging so that View doesnt does its own scrolling logic - local t = self.dragging_scrollbar - self.dragging_scrollbar = false - local r = prev_on_mouse_moved(self, x, y, dx, dy) - self.dragging_scrollbar = t - return r + +function MiniMap:on_mouse_moved(x, y, dx, dy) + local percent = MiniMap.super.on_mouse_moved(self, x, y, dx, dy) + if not self:is_minimap_enabled() or type(percent) ~= "number" then return percent end + local _, visible_lines_count, minimap_lines_start, minimap_lines_count, is_file_too_large = self:get_minimap_dimensions() + local lh = self.dv:get_line_height() + local _, _, w, h = self:get_track_rect() + local tx, ty, tw, th = self:get_thumb_rect() + if x >= tx and x < tx + tw and y >= ty and y < ty + th then self.hovering.thumb = true end + if not self.hovering.thumb then return self.percent end + y = y - self.drag_start_offset + percent = math.max(0.0, math.min((y - self.dv.position.y) / h, 1.0)) + return (((percent * minimap_lines_count) + minimap_lines_start) * lh / self.dv:get_scrollable_size()) - (visible_lines_count / #self.dv.doc.lines / 2) end --- Overloaded since we want the mouse to interact with the full size of the minimap area, --- not juse the scrollbar. -local prev_get_scrollbar_rect = DocView.get_scrollbar_rect -DocView.get_scrollbar_rect = function(self) - if not show_minimap() then return prev_get_scrollbar_rect(self) end +function MiniMap:draw_thumb() + local color = self.hovering.thumb and style.scrollbar2 or style.scrollbar + local x, y, w, h = self:get_thumb_rect() + renderer.draw_rect(x, y, w, h, color) +end - return self.position.x + self.size.x - config.plugins.minimap.width * SCALE, - self.position.y, config.plugins.minimap.width * SCALE, self.size.y +function MiniMap:draw() + if not self:is_minimap_enabled() then return MiniMap.super.draw(self) end + local dv = self.dv + local x, y, w, h = self:get_track_rect() + + local highlight = dv.hovered_scrollbar or dv.dragging_scrollbar + local visual_color = highlight and style.scrollbar2 or style.scrollbar + + local visible_lines_start, visible_lines_count, + minimap_lines_start, minimap_lines_count = self:get_minimap_dimensions() + + if config.plugins.minimap.draw_background then + renderer.draw_rect(x, y, w, h, style.minimap_background or style.background) + end + self:draw_thumb() + + -- highlight the selected lines, and the line with the caret on it + local selection_color = config.plugins.minimap.selection_color or style.dim + local caret_color = config.plugins.minimap.caret_color or style.caret + + for i, line1, col1, line2, col2 in dv.doc:get_selections() do + local selection1_y = y + (line1 - minimap_lines_start) * line_spacing + local selection2_y = y + (line2 - minimap_lines_start) * line_spacing + local selection_min_y = math.min(selection1_y, selection2_y) + local selection_h = math.abs(selection2_y - selection1_y)+1 + renderer.draw_rect(x, selection_min_y, w, selection_h, selection_color) + renderer.draw_rect(x, selection1_y, w, line_spacing, caret_color) + end + + local highlight_align = config.plugins.minimap.highlight_align + local highlight_width = config.plugins.minimap.highlight_width + local gutter_width = config.plugins.minimap.gutter_width + + -- time to draw the actual code, setup some local vars that are used in both highlighted and plain rendering. + local line_y = y + + -- when not using syntax highlighted rendering, just use the normal color but dim it 50%. + local color = style.syntax["normal"] + color = {color[1], color[2], color[3], color[4] * 0.5} + + -- we try to "batch" characters so that they can be rendered as just one rectangle instead of one for each. + local batch_width = 0 + local batch_start = x + local last_batch_end = -1 + local minimap_cutoff_x = config.plugins.minimap.width * SCALE + local batch_syntax_type = nil + local function flush_batch(type, cache) + if batch_width > 0 then + local lastidx = #cache + local old_color = color + color = style.syntax[type] + if config.plugins.minimap.syntax_highlight and color ~= nil then + -- fetch and dim colors + color = {color[1], color[2], color[3], (color[4] or 255) * 0.5} + else + color = old_color + end + if #cache >= 3 then + local last_color = cache[lastidx] + if + last_batch_end == batch_start -- no space skipped + and ( + batch_syntax_type == type -- and same syntax + or ( -- or same color + last_color[1] == color[1] + and last_color[2] == color[2] + and last_color[3] == color[3] + and last_color[4] == color[4] + ) + ) + then + batch_start = cache[lastidx - 2] + batch_width = cache[lastidx - 1] + batch_width + lastidx = lastidx - 3 + end + end + cache[lastidx + 1] = batch_start + cache[lastidx + 2] = batch_width + cache[lastidx + 3] = color + end + batch_syntax_type = type + batch_start = batch_start + batch_width + last_batch_end = batch_start + batch_width = 0 + end + + local highlight_x + if highlight_align == 'left' then + highlight_x = x + else + highlight_x = x + w - highlight_width + end + local function render_highlight(idx, line_y) + local highlight_color = self:line_highlight_color(idx) + if highlight_color then + renderer.draw_rect(highlight_x, line_y, highlight_width, line_spacing, highlight_color) + end + end + + local endidx = math.min(minimap_lines_start + minimap_lines_count, #self.dv.doc.lines) + + reset_cache_if_needed() + + if not highlighter_cache[dv.doc.highlighter] then + highlighter_cache[dv.doc.highlighter] = {} + end + + -- per line + for idx = minimap_lines_start, endidx do + batch_syntax_type = nil + batch_start = 0 + batch_width = 0 + last_batch_end = -1 + + render_highlight(idx, line_y) + local cache = highlighter_cache[dv.doc.highlighter][idx] + if not highlighter_cache[dv.doc.highlighter][idx] then -- need to cache + highlighter_cache[dv.doc.highlighter][idx] = {} + cache = highlighter_cache[dv.doc.highlighter][idx] + -- per token + for _, type, text in dv.doc.highlighter:each_token(idx) do + if not config.plugins.minimap.syntax_highlight then + type = nil + end + local start = 1 + while true do + -- find text followed spaces followed by newline + local s, e, w, eol = string.ufind(text, "[^%s]*()[ \t]*()\n?", start) + if not s then break end + local nchars = w - s + start = e + 1 + batch_width = batch_width + char_spacing * nchars + + local nspaces = 0 + for i=w,e do + local whitespace = string.sub(text, i, i) + if whitespace == "\t" then + nspaces = nspaces + config.plugins.minimap.tab_width + elseif whitespace == " " then + nspaces = nspaces + 1 + end + end + -- not enough spaces; consider them part of the batch + if nspaces < config.plugins.minimap.spaces_to_split then + batch_width = batch_width + nspaces * char_spacing + end + -- line has ended or no more space in the minimap; + -- we can go to the next line + if eol <= w or batch_start + batch_width > minimap_cutoff_x then + if batch_width > 0 then + flush_batch(type, cache) + end + break + end + -- enough spaces to split the batch + if nspaces >= config.plugins.minimap.spaces_to_split then + flush_batch(type, cache) + batch_start = batch_start + nspaces * char_spacing + end + end + end + end + -- draw from cache + for i=1,#cache,3 do + local batch_start = cache[i ] + x + gutter_width + local batch_width = cache[i + 1] + local color = cache[i + 2] + renderer.draw_rect(batch_start, line_y, batch_width, char_height, color) + end + line_y = line_y + line_spacing + end end --- Overloaded so we can render the minimap in the "scrollbar area". -local prev_draw_scrollbar = DocView.draw_scrollbar -DocView.draw_scrollbar = function(self) - if not show_minimap() then return prev_draw_scrollbar(self) end - - local x, y, w, h = self:get_scrollbar_rect() - - local highlight = self.hovered_scrollbar or self.dragging_scrollbar - local visual_color = highlight and style.scrollbar2 or style.scrollbar - - local _, cy, _, cy2 = self:get_content_bounds() - local lh = self:get_line_height() - local visible_lines_count = math.max(1, (cy2 - cy) / lh) - local visible_lines_start = math.max(1, math.floor(cy / lh)) - local scroller_height = visible_lines_count * line_spacing - local line_count = #self.doc.lines - - local visible_y = self.position.y + (visible_lines_start - 1) * line_spacing - - -- check if file is too large to fit inside the minimap area - local max_minmap_lines = math.floor(h / line_spacing) - local minimap_start_line = 1 - if is_file_too_large(self) then - - local scroll_pos = (visible_lines_start - 1) / - (line_count - visible_lines_count - 1) - scroll_pos = math.min(1.0, scroll_pos) -- 0..1, procent of visual area scrolled - - local scroll_pos_pixels = scroll_pos * (h - scroller_height) - visible_y = self.position.y + scroll_pos_pixels - - -- offset visible area if user is scrolling past end - local t = (line_count - visible_lines_start) / visible_lines_count - if t <= 1 then visible_y = visible_y + scroller_height * (1.0 - t) end - - minimap_start_line = visible_lines_start - - math.floor(scroll_pos_pixels / line_spacing) - minimap_start_line = math.max(1, math.min(minimap_start_line, - line_count - max_minmap_lines)) - end - - if config.plugins.minimap.draw_background then - renderer.draw_rect(x, y, w, h, style.minimap_background or style.background) - end - -- draw visual rect - renderer.draw_rect(x, visible_y, w, scroller_height, visual_color) - - -- highlight the selected lines, and the line with the caret on it - local selection_color = config.plugins.minimap.selection_color or style.dim - local caret_color = config.plugins.minimap.caret_color or style.caret - local selection_line, selection_col, selection_line2, selection_col2 = self.doc:get_selection() - local selection_y = y + (selection_line - minimap_start_line) * line_spacing - local selection2_y = y + (selection_line2 - minimap_start_line) * line_spacing - local selection_min_y = math.min(selection_y, selection2_y) - local selection_h = math.abs(selection2_y - selection_y)+1 - renderer.draw_rect(x, selection_min_y, w, selection_h, selection_color) - renderer.draw_rect(x, selection_y, w, line_spacing, caret_color) - - local highlight_align = config.plugins.minimap.highlight_align - local highlight_width = config.plugins.minimap.highlight_width - local gutter_width = config.plugins.minimap.gutter_width - - -- time to draw the actual code, setup some local vars that are used in both highlighted and plain renderind. - local line_y = y - - -- when not using syntax highlighted rendering, just use the normal color but dim it 50%. - local color = style.syntax["normal"] - color = {color[1], color[2], color[3], color[4] * 0.5} - - -- we try to "batch" characters so that they can be rendered as just one rectangle instead of one for each. - local batch_width = 0 - local batch_start = x - local minimap_cutoff_x = x + config.plugins.minimap.width * SCALE - local batch_syntax_type = nil - local function flush_batch(type) - local old_color = color - color = style.syntax[batch_syntax_type] - if config.plugins.minimap.syntax_highlight and color ~= nil then - -- fetch and dim colors - color = {color[1], color[2], color[3], color[4] * 0.5} - else - color = old_color - end - if batch_width > 0 then - renderer.draw_rect(batch_start, line_y, batch_width, char_height, color) - end - batch_syntax_type = type - batch_start = batch_start + batch_width - batch_width = 0 - end - - local highlight_x - if highlight_align == 'left' then - highlight_x = x - else - highlight_x = x + w - highlight_width - end - local function render_highlight(idx, line_y) - local highlight_color = minimap:line_highlight_color(idx) - if highlight_color then - renderer.draw_rect(highlight_x, line_y, highlight_width, line_spacing, highlight_color) - end - end - - -- render lines with syntax highlighting - if config.plugins.minimap.syntax_highlight then - - -- keep track of the highlight type, since this needs to break batches as well - batch_syntax_type = nil - - -- per line - local endidx = minimap_start_line + max_minmap_lines - endidx = math.min(endidx, line_count) - for idx = minimap_start_line, endidx do - batch_syntax_type = nil - batch_start = x + gutter_width - batch_width = 0 - - render_highlight(idx, line_y) - - -- per token - for _, type, text in self.doc.highlighter:each_token(idx) do - -- flush prev batch - if not batch_syntax_type then batch_syntax_type = type end - if batch_syntax_type ~= type then flush_batch(type) end - - -- per character - for char in common.utf8_chars(text) do - if char == " " or char == "\n" then - flush_batch(type) - batch_start = batch_start + char_spacing - elseif char == " " then - flush_batch(type) - batch_start = batch_start + (char_spacing * config.plugins.minimap.tab_width) - elseif batch_start + batch_width > minimap_cutoff_x then - flush_batch(type) - break - else - batch_width = batch_width + char_spacing - end - - end - end - flush_batch(nil) - line_y = line_y + line_spacing - end - - else -- render lines without syntax highlighting - for idx = 1, line_count - 1 do - batch_start = x + gutter_width - batch_width = 0 - - render_highlight(idx, line_y) - - for char in common.utf8_chars(self.doc.lines[idx]) do - if char == " " or char == "\n" then - flush_batch() - batch_start = batch_start + char_spacing - elseif batch_start + batch_width > minimap_cutoff_x then - flush_batch() - else - batch_width = batch_width + char_spacing - end - end - flush_batch() - line_y = line_y + line_spacing - end - - end +local old_docview_new = DocView.new +function DocView:new(doc) + old_docview_new(self, doc) + if self:is(DocView) then self.v_scrollbar = MiniMap(self) end +end + +local old_docview_scroll_to_make_visible = DocView.scroll_to_make_visible +function DocView:scroll_to_make_visible(line, col, ...) + if + not self:is(DocView) or not self.v_scrollbar:is(MiniMap) + or + not self.v_scrollbar:is_minimap_enabled() + then + return old_docview_scroll_to_make_visible(self, line, col, ...) + end + local old_size = self.size.x + self.size.x = math.max(0, self.size.x - config.plugins.minimap.width) + local result = old_docview_scroll_to_make_visible(self, line, col, ...) + self.size.x = old_size + return result end -local prev_update = DocView.update -DocView.update = function (self) - if not show_minimap() then return prev_update(self) end - self.size.x = self.size.x - config.plugins.minimap.width * SCALE - return prev_update(self) + +local function get_all_docviews(node, t) + t = t or {} + if not node then return end + if node.type == "leaf" then + for i,v in ipairs(node.views) do + if v:is(DocView) then + table.insert(t, v) + end + end + end + get_all_docviews(node.a, t) + get_all_docviews(node.b, t) + return t end + command.add(nil, { - ["minimap:toggle-visibility"] = function() - config.plugins.minimap.enabled = not config.plugins.minimap.enabled - end, - ["minimap:toggle-syntax-highlighting"] = function() - config.plugins.minimap.syntax_highlight = not config.plugins.minimap.syntax_highlight - end + ["minimap:toggle-visibility"] = function() + config.plugins.minimap.enabled = not config.plugins.minimap.enabled + for i,v in ipairs(get_all_docviews(core.root_view.root_node)) do + v.v_scrollbar.enabled = nil + end + end, + ["minimap:toggle-syntax-highlighting"] = function() + config.plugins.minimap.syntax_highlight = not config.plugins.minimap.syntax_highlight + end +}) + +command.add("core.docview!", { + ["minimap:toggle-visibility-for-current-view"] = function() + local sb = core.active_view.v_scrollbar + if sb.enabled ~= nil then + sb.enabled = not sb.enabled + else + sb.enabled = not config.plugins.minimap.enabled + end + end }) -return minimap +return MiniMap diff --git a/plugins/motiontrail.lua b/plugins/motiontrail.lua index 1359c90..16e7307 100644 --- a/plugins/motiontrail.lua +++ b/plugins/motiontrail.lua @@ -1,10 +1,34 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" +local common = require "core.common" local style = require "core.style" local DocView = require "core.docview" -config.plugins.motiontrail = { steps = 50 } +config.plugins.motiontrail = common.merge({ + enabled = true, + steps = 50, + -- The config specification used by the settings gui + config_spec = { + name = "Motion Trail", + { + label = "Enabled", + description = "Disable or enable the caret motion trail effect.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Steps", + description = "Amount of trail steps to generate on caret movement.", + path = "steps", + type = "number", + default = 50, + min = 10, + max = 100 + }, + } +}, config.plugins.motiontrail) local function lerp(a, b, t) @@ -14,8 +38,7 @@ end local function get_caret_rect(dv) local line, col = dv.doc:get_selection() - local x, y = dv:get_line_screen_position(line) - x = x + dv:get_col_x_offset(line, col) + local x, y = dv:get_line_screen_position(line, col) return x, y, style.caret_width, dv:get_line_height() end @@ -26,7 +49,9 @@ local draw = DocView.draw function DocView:draw(...) draw(self, ...) - if self ~= core.active_view then return end + if not config.plugins.motiontrail.enabled or self ~= core.active_view then + return + end local x, y, w, h = get_caret_rect(self) diff --git a/plugins/navigate.lua b/plugins/navigate.lua index 4e6092f..d83c02f 100644 --- a/plugins/navigate.lua +++ b/plugins/navigate.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local common = require "core.common" diff --git a/plugins/nonicons.lua b/plugins/nonicons.lua index 9a1e963..b8b01bc 100644 --- a/plugins/nonicons.lua +++ b/plugins/nonicons.lua @@ -1,11 +1,57 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +-- Author: Jipok +-- Doesn't work well with scaling mode == "ui" + local core = require "core" local common = require "core.common" +local config = require "core.config" local style = require "core.style" local TreeView = require "plugins.treeview" +local Node = require "core.node" +-- Config +config.plugins.nonicons = common.merge({ + use_default_dir_icons = false, + use_default_chevrons = false, + draw_treeview_icons = true, + draw_tab_icons = true, + -- The config specification used by the settings gui + config_spec = { + name = "Nonicons", + { + label = "Use Default Directory Icons", + description = "When enabled does not use nonicon directory icons.", + path = "use_default_dir_icons", + type = "toggle", + default = false + }, + { + label = "Use Default Chevrons", + description = "When enabled does not use nonicon expand/collapse arrow icons.", + path = "use_default_chevrons", + type = "toggle", + default = false + }, + { + label = "Draw Treeview Icons", + description = "Enables file related icons on the treeview.", + path = "draw_treeview_icons", + type = "toggle", + default = true + }, + { + label = "Draw Tab Icons", + description = "Adds file related icons to tabs.", + path = "draw_tab_icons", + type = "toggle", + default = true + } + } +}, config.plugins.nonicons) local icon_font = renderer.font.load(USERDIR.."/fonts/nonicons.ttf", 15 * SCALE) +local chevron_width = icon_font:get_width("") +local previous_scale = SCALE local extension_icons = { [".lua"] = { "#51a0cf", "" }, [".md"] = { "#519aba", "" }, -- Markdown @@ -39,16 +85,16 @@ local extension_icons = { [".swift"] = { "#e37933", "" }, [".ts"] = { "#519aba", "" }, -- TypeScript [".elm"] = { "#519aba", "" }, - [".diff"] = { "#41535b", "" }, [".patch"] = { "#41535b", "" }, + [".diff"] = { "#41535b", "" }, [".ex"] = { "#a074c4", "" }, [".exs"] = { "#a074c4", "" }, -- Elixir -- Following without special icon: [".awk"] = { "#4d5a5e", "" }, [".nim"] = { "#F88A02", "" }, [".zig"] = { "#cbcb41", "" }, - } local known_names_icons = { ["changelog"] = { "#657175", "" }, ["changelog.txt"] = { "#4d5a5e", "" }, + ["changelog.md"] = { "#519aba", "" }, ["makefile"] = { "#6d8086", "" }, ["dockerfile"] = { "#296478", "" }, ["docker-compose.yml"] = { "#4289a1", "" }, @@ -68,66 +114,68 @@ for k, v in pairs(known_names_icons) do v[1] = { common.color(v[1]) } end --- Replace original draw -function TreeView:draw() - if not self.visible then return end - self:draw_background(style.background2) - - local icon_width = icon_font:get_width("") - local spacing = icon_font:get_width("") / 2 - - local doc = core.active_view.doc - local active_filename = doc and system.absolute_path(doc.filename or "") - - for item, x,y,w,h in self:each_item() do - local color = style.text - - -- highlight active_view doc - if item.abs_filename == active_filename then - color = style.accent +-- Override function to change default icons for dirs, special extensions and names +local TreeView_get_item_icon = TreeView.get_item_icon +function TreeView:get_item_icon(item, active, hovered) + local icon, font, color = TreeView_get_item_icon(self, item, active, hovered) + if previous_scale ~= SCALE then + icon_font:set_size( + icon_font:get_size() * (SCALE / previous_scale) + ) + chevron_width = icon_font:get_width("") + previous_scale = SCALE + end + if not config.plugins.nonicons.use_default_dir_icons then + icon = "" -- unicode 61766 + font = icon_font + color = style.text + if item.type == "dir" then + icon = item.expanded and "" or "" -- unicode U+F23C and U+F23B end - - -- hovered item background - if item == self.hovered_item then - renderer.draw_rect(x, y, w, h, style.line_highlight) + end + if config.plugins.nonicons.draw_treeview_icons then + local custom_icon = known_names_icons[item.name:lower()] + if custom_icon == nil then + custom_icon = extension_icons[item.name:match("^.+(%..+)$")] + end + if custom_icon ~= nil then + color = custom_icon[1] + icon = custom_icon[2] + font = icon_font + end + if active or hovered then color = style.accent end + end + return icon, font, color +end - -- icons - x = x + item.depth * style.padding.x + style.padding.x +-- Override function to draw chevrons if setting is disabled +local TreeView_draw_item_chevron = TreeView.draw_item_chevron +function TreeView:draw_item_chevron(item, active, hovered, x, y, w, h) + if not config.plugins.nonicons.use_default_chevrons then if item.type == "dir" then - local icon1 = item.expanded and "" or "" -- unicode 61726 and 61728 - local icon2 = item.expanded and "" or "" -- unicode U+F23C and U+F23B - x = x - spacing - common.draw_text(icon_font, color, icon1, nil, x, y, 0, h) - x = x + style.padding.x + spacing - common.draw_text(icon_font, color, icon2, nil, x, y, 0, h) - x = x + icon_width - else - x = x + style.padding.x - -- default icon - local icon = "" -- unicode 61766 - local icon_color = color - -- icon depending on the file extension or full name - local custom_icon = known_names_icons[item.name:lower()] - if custom_icon == nil then - custom_icon = extension_icons[item.name:match("^.+(%..+)$")] - end - if custom_icon ~= nil then - icon_color = custom_icon[1] - icon = custom_icon[2] - end - common.draw_text(icon_font, icon_color, icon, nil, x, y, 0, h) - x = x + icon_width + local chevron_icon = item.expanded and "" or "" + local chevron_color = hovered and style.accent or style.text + common.draw_text(icon_font, chevron_color, chevron_icon, nil, x, y, 0, h) end - - -- text - x = x + spacing - x = common.draw_text(style.font, color, item.name, nil, x, y, 0, h) + return chevron_width + style.padding.x/4 end + return TreeView_draw_item_chevron(self, item, active, hovered, x, y, w, h) +end - self:draw_scrollbar() - if self.hovered_item and self.tooltip.alpha > 0 then - core.root_view:defer_draw(self.draw_tooltip, self) +-- Override function to draw icons in tabs titles if setting is enabled +local Node_draw_tab_title = Node.draw_tab_title +function Node:draw_tab_title(view, font, is_active, is_hovered, x, y, w, h) + if config.plugins.nonicons.draw_tab_icons then + local padx = chevron_width + style.padding.x/2 + local tx = x + padx -- Space for icon + w = w - padx + Node_draw_tab_title(self, view, font, is_active, is_hovered, tx, y, w, h) + if (view == nil) or (view.doc == nil) then return end + local item = { type = "file", name = view.doc:get_name() } + TreeView:draw_item_icon(item, false, is_hovered, x, y, w, h) + else + Node_draw_tab_title(self, view, font, is_active, is_hovered, x, y, w, h) end end diff --git a/plugins/opacity.lua b/plugins/opacity.lua index 8dd0d9a..a97cae9 100644 --- a/plugins/opacity.lua +++ b/plugins/opacity.lua @@ -1,4 +1,5 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local core = require "core" local common = require "core.common" local command = require "core.command" local keymap = require "core.keymap" @@ -11,7 +12,7 @@ local default_opacity = 1 local current_opacity = default_opacity local function set_opacity(opacity) - if not opacity_on then opacity_on = true end + if not opacity_on then return end current_opacity = common.clamp(opacity, 0.2, 1) system.set_window_opacity(current_opacity) end @@ -30,8 +31,10 @@ end local function tog_opacity() opacity_on = not opacity_on if opacity_on then + core.log("Opacity: on") system.set_window_opacity(current_opacity) else + core.log("Opacity: off") system.set_window_opacity(default_opacity) end end @@ -53,7 +56,14 @@ command.add(nil, { ["opacity:reset" ] = function() res_opacity() end, ["opacity:decrease"] = function() dec_opacity() end, ["opacity:increase"] = function() inc_opacity() end, - ["opacity:toggle mouse wheel use"] = function() use_mousewheel = not use_mousewheel end, + ["opacity:toggle mouse wheel use"] = function() + use_mousewheel = not use_mousewheel + if use_mousewheel then + core.log("Opacity (shift + mouse wheel): on") + else + core.log("Opacity (shift + mouse wheel): off") + end + end, }) keymap.add { diff --git a/plugins/open_ext.lua b/plugins/open_ext.lua index 8a98516..4c57d68 100644 --- a/plugins/open_ext.lua +++ b/plugins/open_ext.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- The general idea is to check if the file opened is valid utf-8 -- since lite-xl only supports UTF8 text, others can be safely assumed diff --git a/plugins/openfilelocation.lua b/plugins/openfilelocation.lua index 4b89815..603c7b6 100644 --- a/plugins/openfilelocation.lua +++ b/plugins/openfilelocation.lua @@ -1,18 +1,32 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" +local common = require "core.common" local command = require "core.command" local config = require "core.config" - -config.plugins.openfilelocation = {} +local platform_filemanager if PLATFORM == "Windows" then - config.plugins.openfilelocation.filemanager = "explorer" + platform_filemanager = "explorer" elseif PLATFORM == "Mac OS X" then - config.plugins.openfilelocation.filemanager = "open" + platform_filemanager = "open" else - config.plugins.openfilelocation.filemanager = "xdg-open" + platform_filemanager = "xdg-open" end +config.plugins.openfilelocation = common.merge({ + filemanager = platform_filemanager, + -- The config specification used by the settings gui + config_spec = { + name = "Open File Location", + { + label = "File Manager", + description = "Command of the file browser.", + path = "filemanager", + type = "string", + default = platform_filemanager + } + } +}, config.plugins.openfilelocation) command.add("core.docview", { ["open-file-location:open-file-location"] = function() diff --git a/plugins/openselected.lua b/plugins/openselected.lua index af00194..6333da9 100644 --- a/plugins/openselected.lua +++ b/plugins/openselected.lua @@ -1,19 +1,35 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" +local common = require "core.common" local config = require "core.config" +local contextmenu = require "plugins.contextmenu" -config.plugins.openselected = {} +local platform_filelauncher if PLATFORM == "Windows" then - config.plugins.openselected.filemanager = "start" + platform_filelauncher = "start" elseif PLATFORM == "Mac OS X" then - config.plugins.openselected.filemanager = "open" + platform_filelauncher = "open" else - config.plugins.openselected.filemanager = "xdg-open" + platform_filelauncher = "xdg-open" end +config.plugins.openselected = common.merge({ + filelauncher = platform_filelauncher, + -- The config specification used by the settings gui + config_spec = { + name = "Open Selected Text", + { + label = "File Launcher", + description = "Command used to open the selected path or link externally.", + path = "filelauncher", + type = "string", + default = platform_filelauncher + } + } +}, config.plugins.openselected) command.add("core.docview", { ["open-selected:open-selected"] = function() @@ -35,10 +51,16 @@ command.add("core.docview", { core.log("Opening %s...", text) - system.exec(config.plugins.openselected.filemanager .. " " .. text) + system.exec(config.plugins.openselected.filelauncher .. " " .. text) end, }) -keymap.add { ["ctrl+shift+o"] = "open-selected:open-selected" } +contextmenu:register("core.docview", { + contextmenu.DIVIDER, + { text = "Open Selection", command = "open-selected:open-selected" } +}) + + +keymap.add { ["ctrl+alt+o"] = "open-selected:open-selected" } diff --git a/plugins/pdfview.lua b/plugins/pdfview.lua index d5d749a..199584e 100644 --- a/plugins/pdfview.lua +++ b/plugins/pdfview.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/plugins/primary_selection.lua b/plugins/primary_selection.lua new file mode 100644 index 0000000..388caf0 --- /dev/null +++ b/plugins/primary_selection.lua @@ -0,0 +1,187 @@ +-- mod-version:3 +local core = require "core" +local Doc = require "core.doc" +local command = require "core.command" +local keymap = require "core.keymap" +local config = require "core.config" +local common = require "core.common" + +local function string_to_cmd(s) + local result = {} + for match in s:gmatch("%g+") do + table.insert(result, match) + end + return result +end + +config.plugins.primary_selection = common.merge({ + command_in = { "xclip", "-in", "-selection", "primary" }, -- Command to use to copy the selection + command_out = { "xclip", "-out", "-selection", "primary" }, -- Command to use to obtain the selection + set_cursor = true, -- Set cursor on middle mouse click + min_copy_time = 0.150, -- How much time to delay setting the selection; in seconds + config_spec = { + name = "Primary selection", + { + label = "Command copy", + description = "Command to use to copy the selection.", + path = "_command_in", + type = "string", + default = "xclip -in -selection primary", + on_apply = function(value) + config.plugins.primary_selection.command_in = string_to_cmd(value) + end, + }, + { + label = "Command paste", + description = "Command to use to obtain the selection.", + path = "_command_out", + type = "string", + default = "xclip -out -selection primary", + on_apply = function(value) + config.plugins.primary_selection.command_out = string_to_cmd(value) + end, + }, + { + label = "Set cursor", + description = "Set cursor on middle mouse click.", + path = "set_cursor", + type = "toggle", + default = true, + }, + { + label = "Copy timeout", + description = "How much time to delay setting the selection; in milliseconds.", + path = "min_copy_time_ms", + type = "number", + default = 150, + min = 0, + step = 50, + on_apply = function(value) + config.plugins.primary_selection.min_copy_time = value / 1000 + end + }, + } +}, config.plugins.primary_selection) + + +local last_selection_data +--[[ + = { + time = nil, + line1 = nil, + col1 = nil, + line2 = nil, + col2 = nil, + doc = nil, +} +]] + +local xclip_copy +local function delayed_copy() + while true do + local data = last_selection_data + if not data then return end + local current_time = system.get_time() + local diff_time = current_time - data.time + -- Check if enough time has passed since last selection change + if diff_time >= config.plugins.primary_selection.min_copy_time then + if xclip_copy then xclip_copy:terminate() end + if not config.plugins.primary_selection.command_in + or #config.plugins.primary_selection.command_in == 0 then + core.warn("No primary selection copy command set") + break + end + xclip_copy = process.start(config.plugins.primary_selection.command_in) + if not xclip_copy then + core.warn("Unable to start copy command") + break + end + local text = data.doc:get_text(data.line1, data.col1, data.line2, data.col2) + local nbytes = #text + local total_written = 0 + -- In some rare cases xclip isn't fast enough so we need to retry sending the data + local retry = 3 + repeat + local written, err = xclip_copy:write(text) + if written == 0 or not written then + if retry > 0 then + retry = retry - 1 + else + core.error("Error while setting primary selection. "..(err or "")) + break + end + else + retry = 3 + end + total_written = total_written + written + text = string.sub(text, written + 1) + until total_written >= nbytes + xclip_copy:close_stream(process.STREAM_STDIN) + -- We need to leave the process running as killing it would destroy the copied buffer + break + end + coroutine.yield() + end + last_selection_data = nil +end + + +local doc_set_selections = Doc.set_selections +function Doc:set_selections(...) + local result = doc_set_selections(self, ...) + local line1, col1, line2, col2 + line1, col1, line2, col2 = self:get_selection() + if line1 ~= line2 or col1 ~= col2 then + if not last_selection_data then + -- Start "timer" to confirm the selection only after `min_copy_time` has passed + core.add_thread(delayed_copy) + last_selection_data = { } + end + -- We could extract the text here, but it is a potentially heavy operation, + -- so we do it only when we're actually confirming the selection. + -- The drawback is that if the selection is overwritten/deleted, + -- it is either never sent, or is different than expected. + -- TODO: Confirm the selection on text change. + last_selection_data.time = system.get_time() + last_selection_data.line1 = line1 + last_selection_data.col1 = col1 + last_selection_data.line2 = line2 + last_selection_data.col2 = col2 + last_selection_data.doc = self + end + return result +end + + +command.add("core.docview", { + ["primary-selection:paste"] = function(x, y, clicks, ...) + if not config.plugins.primary_selection.command_out + or #config.plugins.primary_selection.command_out == 0 then + core.warn("No primary selection paste command set") + return + end + if x and config.plugins.primary_selection.set_cursor then + -- TODO: There must be a better way to do this + core.on_event("mousepressed", "left", x, y, clicks, ...) + core.on_event("mousereleased", "left", x, y, clicks, ...) + end + local xclip = process.start(config.plugins.primary_selection.command_out) + if not xclip then + core.warn("Unable to start paste command") + return + end + local text = {} + repeat + local buffer = xclip:read_stdout() + table.insert(text, buffer or "") + until not buffer + if #text > 0 then + core.active_view.doc:text_input(table.concat(text)) + end + end +}) + +keymap.add({ + ["1mclick"] = "primary-selection:paste" +}) + diff --git a/plugins/rainbowparen.lua b/plugins/rainbowparen.lua index 52e9d50..6ca4cb4 100644 --- a/plugins/rainbowparen.lua +++ b/plugins/rainbowparen.lua @@ -1,7 +1,23 @@ --- mod-version:2 -- lite-xl 2.0 -local tokenizer = require "core.tokenizer" +-- mod-version:3 +local core = require "core" local style = require "core.style" +local config = require "core.config" local common = require "core.common" +local command = require "core.command" +local tokenizer = require "core.tokenizer" +local Highlighter = require "core.doc.highlighter" + +config.plugins.rainbowparen = common.merge({ + enabled = true, + parens = 5 +}, config.plugins.rainbowparen) + +style.syntax.paren_unbalanced = style.syntax.paren_unbalanced or { common.color "#DC0408" } +style.syntax.paren1 = style.syntax.paren1 or { common.color "#FC6F71"} +style.syntax.paren2 = style.syntax.paren2 or { common.color "#fcb053"} +style.syntax.paren3 = style.syntax.paren3 or { common.color "#fcd476"} +style.syntax.paren4 = style.syntax.paren4 or { common.color "#52dab2"} +style.syntax.paren5 = style.syntax.paren5 or { common.color "#5a98cf"} local tokenize = tokenizer.tokenize local closers = { @@ -9,10 +25,15 @@ local closers = { ["["] = "]", ["{"] = "}" } + local function parenstyle(parenstack) - return "paren" .. ((#parenstack % 5) + 1) + return "paren" .. ((#parenstack % config.plugins.rainbowparen.parens) + 1) end + function tokenizer.tokenize(syntax, text, state) + if not config.plugins.rainbowparen.enabled then + return tokenize(syntax, text, state) + end state = state or {} local res, istate = tokenize(syntax, text, state.istate) local parenstack = state.parenstack or "" @@ -51,9 +72,31 @@ function tokenizer.tokenize(syntax, text, state) return newres, { parenstack = parenstack, istate = istate } end -style.syntax.paren_unbalanced = style.syntax.paren_unbalanced or { common.color "#DC0408" } -style.syntax.paren1 = style.syntax.paren1 or { common.color "#FC6F71"} -style.syntax.paren2 = style.syntax.paren2 or { common.color "#fcb053"} -style.syntax.paren3 = style.syntax.paren3 or { common.color "#fcd476"} -style.syntax.paren4 = style.syntax.paren4 or { common.color "#52dab2"} -style.syntax.paren5 = style.syntax.paren5 or { common.color "#5a98cf"} +local function toggle_rainbowparen(enabled) + config.plugins.rainbowparen.enabled = enabled + for _, doc in ipairs(core.docs) do + doc.highlighter = Highlighter(doc) + doc:reset_syntax() + end +end + +-- The config specification used by the settings gui +config.plugins.rainbowparen.config_spec = { + name = "Rainbow Parentheses", + { + label = "Enable", + description = "Activates rainbow parenthesis coloring by default.", + path = "enabled", + type = "toggle", + default = true, + on_apply = function(enabled) + toggle_rainbowparen(enabled) + end + } +} + +command.add(nil, { + ["rainbow-parentheses:toggle"] = function() + toggle_rainbowparen(not config.plugins.rainbowparen.enabled) + end +}) diff --git a/plugins/regexreplacepreview.lua b/plugins/regexreplacepreview.lua index 1c8b845..18d692b 100644 --- a/plugins/regexreplacepreview.lua +++ b/plugins/regexreplacepreview.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local keymap = require "core.keymap" local command = require "core.command" @@ -6,121 +6,125 @@ local command = require "core.command" -- Takes the following pattern: /pattern/replace/ -- Capture groupings can be replaced using \1 through \9 local function regex_replace_file(view, pattern, old_lines, raw, start_line, end_line) - local doc = view.doc - local start_pattern, end_pattern, end_replacement, start_replacement = 2, 2; - repeat - end_pattern = string.find(pattern, "/", end_pattern) - until end_pattern == nil or pattern[end_pattern-1] ~= "\\" - if end_pattern == nil then - end_pattern = #pattern + 1 - else - end_pattern = end_pattern - 1 - start_replacement = end_pattern+2; - end_replacement = end_pattern+2; - repeat - end_replacement = string.find(pattern, "/", end_replacement) - until end_replacement == nil or pattern[end_replacement-1] ~= "\\" - end - end_replacement = end_replacement and (end_replacement - 1) - - local re = start_pattern ~= end_pattern and regex.compile(pattern:sub(start_pattern, end_pattern)) - - local replacement = end_replacement and pattern:sub(start_replacement, end_replacement) - local replace_line = raw and function(line, new_text) - if line == #doc.lines then - doc:raw_remove(line, 1, line, #doc.lines[line], { idx = 1 }, 0) - else - doc:raw_remove(line, 1, line+1, 1, { idx = 1 }, 0) - end - doc:raw_insert(line, 1, new_text, { idx = 1 }, 0) - end or function(line, new_text) - if line == #doc.lines then - doc:remove(line, 1, line, #doc.lines[line]) - else - doc:remove(line, 1, line+1, 1) - end - doc:insert(line, 1, new_text) - end - - local line_scroll = nil - if re then - for i = (start_line or 1), (end_line or #doc.lines) do - local new_text, matches, rmatches - local old_text = old_lines[i] or doc.lines[i] - local old_length = #old_text - if replacement then - new_text, matches, rmatches = regex.gsub(re, old_text, replacement) - end - if matches and #matches > 0 then - old_lines[i] = old_text - replace_line(i, new_text) - if line_scroll == nil then - line_scroll = i - doc:set_selection(i, rmatches[1][1], i, rmatches[1][2]) - end - elseif old_lines[i] then - replace_line(i, old_lines[i]) - old_lines[i] = nil - end - if not replacement then - local s,e = regex.match(re, old_text) - if s then - line_scroll = i - doc:set_selection(i, s, i, e) - break - end - end + local doc = view.doc + local start_pattern, end_pattern, end_replacement, start_replacement = 2, 2; + repeat + end_pattern = string.find(pattern, "/", end_pattern) + until end_pattern == nil or pattern[end_pattern-1] ~= "\\" + if end_pattern == nil then + end_pattern = #pattern + 1 + else + end_pattern = end_pattern - 1 + start_replacement = end_pattern+2; + end_replacement = end_pattern+2; + repeat + end_replacement = string.find(pattern, "/", end_replacement) + until end_replacement == nil or pattern[end_replacement-1] ~= "\\" + end + end_replacement = end_replacement and (end_replacement - 1) + + local re = start_pattern ~= end_pattern + and regex.compile(pattern:sub(start_pattern, end_pattern)) + + local replacement = end_replacement and pattern:sub( + start_replacement, end_replacement + ) + local replace_line = raw and function(line, new_text) + if line == #doc.lines then + doc:raw_remove(line, 1, line, #doc.lines[line], { idx = 1 }, 0) + else + doc:raw_remove(line, 1, line+1, 1, { idx = 1 }, 0) + end + doc:raw_insert(line, 1, new_text, { idx = 1 }, 0) + end or function(line, new_text) + if line == #doc.lines then + doc:remove(line, 1, line, #doc.lines[line]) + else + doc:remove(line, 1, line+1, 1) + end + doc:insert(line, 1, new_text) + end + + local line_scroll = nil + if re then + for i = (start_line or 1), (end_line or #doc.lines) do + local new_text, matches, rmatches + local old_text = old_lines[i] or doc.lines[i] + local old_length = #old_text + if replacement then + new_text, matches, rmatches = regex.gsub(re, old_text, replacement) end - if line_scroll then - view:scroll_to_line(line_scroll, true) + if matches and #matches > 0 then + old_lines[i] = old_text + replace_line(i, new_text) + if line_scroll == nil then + line_scroll = i + doc:set_selection(i, rmatches[1][1], i, rmatches[1][2]) + end + elseif old_lines[i] then + replace_line(i, old_lines[i]) + old_lines[i] = nil end - end - if replacement == nil then - for k,v in pairs(old_lines) do - replace_line(k, v) + if not replacement then + local s,e = regex.match(re, old_text) + if s then + line_scroll = i + doc:set_selection(i, s, i, e) + break + end end - old_lines = {} - end - return old_lines, line_scroll ~= nil + end + if line_scroll then + view:scroll_to_line(line_scroll, true) + end + end + if replacement == nil then + for k,v in pairs(old_lines) do + replace_line(k, v) + end + old_lines = {} + end + return old_lines, line_scroll ~= nil end command.add("core.docview", { - ["regex-replace-preview:find-replace-regex"] = function() - core.command_view:set_text("/") - local old_lines = {} - local view = core.active_view - local doc = view.doc - local original_selection = { doc:get_selection(true) } - local selection = doc:has_selection() and { doc:get_selection(true) } or {} - core.command_view:enter( - "Regex Replace (enter pattern as /old/new/)", - function(pattern) - regex_replace_file(view, pattern, {}, false, selection[1], selection[3]) - end, - function(pattern) - local incremental, has_replacement = regex_replace_file(view, pattern, old_lines, true, selection[1], selection[3]) - if incremental then - old_lines = incremental - end - if not has_replacement then - doc:set_selection(unpack(original_selection)) + ["regex-replace-preview:find-replace-regex"] = function() + local old_lines = {} + local view = core.active_view + local doc = view.doc + local original_selection = { doc:get_selection(true) } + local selection = doc:has_selection() and { doc:get_selection(true) } or {} + core.command_view:enter("Regex Replace (enter pattern as /old/new/)", { + text = "/", + submit = function(pattern) + regex_replace_file(view, pattern, {}, false, selection[1], selection[3]) + end, + suggest = function(pattern) + local incremental, has_replacement = regex_replace_file( + view, pattern, old_lines, true, selection[1], selection[3] + ) + if incremental then + old_lines = incremental + end + if not has_replacement then + doc:set_selection(table.unpack(original_selection)) + end + end, + cancel = function(pattern) + for k,v in pairs(old_lines) do + if v then + if k == #doc.lines then + doc:raw_remove(k, 1, k, #doc.lines[k], { idx = 1 }, 0) + else + doc:raw_remove(k, 1, k+1, 1, { idx = 1 }, 0) end - end, - function(pattern) - for k,v in pairs(old_lines) do - if v then - if k == #doc.lines then - doc:raw_remove(k, 1, k, #doc.lines[k], { idx = 1 }, 0) - else - doc:raw_remove(k, 1, k+1, 1, { idx = 1 }, 0) - end - doc:raw_insert(k, 1, v, { idx = 1 }, 0) - end - end - doc:set_selection(unpack(original_selection)) - end - ) - end + doc:raw_insert(k, 1, v, { idx = 1 }, 0) + end + end + doc:set_selection(table.unpack(original_selection)) + end + }) + end }) keymap.add { ["ctrl+shift+r"] = "regex-replace-preview:find-replace-regex" } diff --git a/plugins/restoretabs.lua b/plugins/restoretabs.lua index 5bcd977..4c33304 100644 --- a/plugins/restoretabs.lua +++ b/plugins/restoretabs.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 -- Not perfect, because we can't actually figure out when something closes, but should be good enough, so long as we check the list of open views. -- Maybe find a better way to get at "Node"? local core = require "core" @@ -17,7 +17,7 @@ RootView.update = function(self) if not initialized_tab_system then local Node = getmetatable(self.root_node) local old_close = Node.close_view - + Node.close_view = function(self, root, view) if view.doc and view.doc.abs_filename then local closing_filename = view.doc.abs_filename @@ -40,7 +40,7 @@ RootView.update = function(self) end -command.add("core.docview", { +command.add(nil, { ["restore-tabs:restore-tab"] = function() if #tab_history > 0 then local file = tab_history[#tab_history] diff --git a/plugins/scalestatus.lua b/plugins/scalestatus.lua index 8f3ef68..a7623b1 100644 --- a/plugins/scalestatus.lua +++ b/plugins/scalestatus.lua @@ -1,34 +1,54 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 --[[ scalestatus.lua displays current scale (zoom) in status view version: 20200628_155804 originally by SwissalpS --]] -local scale = require "plugins.scale" - +local core = require "core" +local common = require "core.common" local config = require "core.config" +local scale = require "plugins.scale" local StatusView = require "core.statusview" -config.plugins.scalestatus = { format = '%.0f%%' } - -local get_items = StatusView.get_items -function StatusView:get_items() - - local left, right = get_items(self) - - local t = { - self.separator, - string.format(config.plugins.scalestatus.format, scale.get() * 100), +config.plugins.scalestatus = common.merge({ + enabled = true, + format = '%.0f%%', + -- The config specification used by the settings gui + config_spec = { + name = "Scale Status", + { + label = "Enabled", + description = "Show or hide the scale status from the status bar.", + path = "enabled", + type = "toggle", + default = true, + on_apply = function(enabled) + core.add_thread(function() + if enabled then + core.status_view:get_item("status:scale"):show() + else + core.status_view:get_item("status:scale"):hide() + end + end) + end + } } - - for _, item in ipairs(t) do - table.insert(right, item) - end - - return left, right - -end +}, config.plugins.scalestatus) + +core.status_view:add_item({ + name = "status:scale", + alignment = StatusView.Item.RIGHT, + get_item = function() + return {string.format( + config.plugins.scalestatus.format, + scale.get() * 100 + )} + end, + position = 1, + tooltip = "scale", + separator = core.status_view.separator2 +}) return true diff --git a/plugins/select_colorscheme.lua b/plugins/select_colorscheme.lua index 1e25bc4..6fa45d4 100644 --- a/plugins/select_colorscheme.lua +++ b/plugins/select_colorscheme.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local common = require "core.common" @@ -65,10 +65,10 @@ local function make_color_module_name(name) end function Settings:change_color(name) - if self:is_change_color(name) then - core.reload_module(make_color_module_name(name)) - self.color_scheme = name - end + if self:is_change_color(name) then + core.reload_module(make_color_module_name(name)) + self.color_scheme = name + end end function Settings:save_settings() @@ -121,10 +121,12 @@ local color_scheme_suggest = function(text) end command.add(nil, { - ["ui:color scheme"] = function() - core.command_view:enter("Select color scheme", color_scheme_submit, color_scheme_suggest) - end, - }) + ["ui:color scheme"] = function() + core.command_view:enter("Select color scheme", { + submit = color_scheme_submit, suggest = color_scheme_suggest + }) + end, +}) -- ---------------------------------------------------------------- Settings:init() diff --git a/plugins/selectionhighlight.lua b/plugins/selectionhighlight.lua index 19bc475..133dced 100644 --- a/plugins/selectionhighlight.lua +++ b/plugins/selectionhighlight.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local style = require "core.style" local DocView = require "core.docview" @@ -16,15 +16,15 @@ end local draw_line_body = DocView.draw_line_body -function DocView:draw_line_body(idx, x, y) - draw_line_body(self, idx, x, y) +function DocView:draw_line_body(line, x, y) + local line_height = draw_line_body(self, line, x, y) local line1, col1, line2, col2 = self.doc:get_selection(true) if line1 == line2 and col1 ~= col2 then local selection = self.doc:get_text(line1, col1, line2, col2) if not selection:match("^%s+$") then local lh = self:get_line_height() local selected_text = self.doc.lines[line1]:sub(col1, col2 - 1) - local current_line_text = self.doc.lines[idx] + local current_line_text = self.doc.lines[line] local last_col = 1 while true do local start_col, end_col = current_line_text:find( @@ -32,9 +32,9 @@ function DocView:draw_line_body(idx, x, y) ) if start_col == nil then break end -- don't draw box around the selection - if idx ~= line1 or start_col ~= col1 then - local x1 = x + self:get_col_x_offset(idx, start_col) - local x2 = x + self:get_col_x_offset(idx, end_col + 1) + if line ~= line1 or start_col ~= col1 then + local x1 = x + self:get_col_x_offset(line, start_col) + local x2 = x + self:get_col_x_offset(line, end_col + 1) local color = style.selectionhighlight or style.syntax.comment draw_box(x1, y, x2 - x1, lh, color) end @@ -42,5 +42,6 @@ function DocView:draw_line_body(idx, x, y) end end end + return line_height end diff --git a/plugins/settings.lua b/plugins/settings.lua new file mode 100644 index 0000000..62d1592 --- /dev/null +++ b/plugins/settings.lua @@ -0,0 +1,1858 @@ +-- mod-version:3 --priority:0 +local core = require "core" +local config = require "core.config" +local common = require "core.common" +local command = require "core.command" +local keymap = require "core.keymap" +local style = require "core.style" + +-- check if widget is installed before proceeding +local widget_found, Widget = pcall(require, "widget") +if not widget_found then + core.error("Widget library not found: https://github.com/lite-xl/lite-xl-widgets") + return +end + +local Label = require "widget.label" +local Line = require "widget.line" +local NoteBook = require "widget.notebook" +local Button = require "widget.button" +local TextBox = require "widget.textbox" +local SelectBox = require "widget.selectbox" +local NumberBox = require "widget.numberbox" +local Toggle = require "widget.toggle" +local ListBox = require "widget.listbox" +local FoldingBook = require "widget.foldingbook" +local FontsList = require "widget.fontslist" +local ItemsList = require "widget.itemslist" +local KeybindingDialog = require "widget.keybinddialog" +local Fonts = require "widget.fonts" +local FilePicker = require "widget.filepicker" + +local settings = {} + +settings.core = {} +settings.plugins = {} +settings.sections = {} +settings.plugin_sections = {} +settings.config = {} +settings.default_keybindings = {} + +---Enumeration for the different types of settings. +---@type table<string, integer> +settings.type = { + STRING = 1, + NUMBER = 2, + TOGGLE = 3, + SELECTION = 4, + LIST_STRINGS = 5, + BUTTON = 6, + FONT = 7, + FILE = 8, + DIRECTORY = 9 +} + +---@alias settings.types +---| `settings.type.STRING` +---| `settings.type.NUMBER` +---| `settings.type.TOGGLE` +---| `settings.type.SELECTION` +---| `settings.type.LIST_STRINGS` +---| `settings.type.BUTTON` +---| `settings.type.FONT` +---| `settings.type.FILE` + +---Represents a setting to render on a settings pane. +---@class settings.option +---@field public label string +---@field public description string +---@field public path string +---@field public type settings.types | integer +---@field public default string | number | boolean | table<integer, string> | table<integer, integer> +---@field public min number +---@field public max number +---@field public step number +---@field public values table +---@field public fonts_list table<string, renderer.font> +---@field public font_error boolean +---@field public get_value nil | fun(value:any):any +---@field public set_value nil | fun(value:any):any +---@field public icon string +---@field public on_click nil | string | fun(button:string, x:integer, y:integer) +---@field public on_apply nil | fun(value:any) +---@field public exists boolean +---@field public filters table<integer,string> +settings.option = { + ---Title displayed to the user eg: "My Option" + label = "", + ---Description of the option eg: "Modifies the document indentation" + description = "", + ---Config path in the config table, eg: section.myoption, myoption, etc... + path = "", + ---Type of option that will be used to render an appropriate control + type = "", + ---Default value of the option + default = "", + ---Used for NUMBER to indiciate the minimum number allowed + min = 0, + ---Used for NUMBER to indiciate the maximum number allowed + max = 0, + ---Used for NUMBER to indiciate the increment/decrement amount + step = 0, + ---Used in a SELECTION to provide the list of valid options + values = {}, + ---Optionally used for FONT to store the generated font group. + fonts_list = {}, + ---Flag set to true when loading user defined fonts fail + font_error = false, + ---Optional function that is used to manipulate the current value on retrieval. + get_value = nil, + ---Optional function that is used to manipulate the saved value on save. + set_value = nil, + ---The icon set for a BUTTON + icon = "", + ---Command or function executed when a BUTTON is clicked + on_click = nil, + ---Optional function executed when the option value is applied. + on_apply = nil, + ---When FILE or DIRECTORY this flag tells the path should exist. + exists = false, + ---Lua patterns used on FILE or DIRECTORY to filter browser results and + ---also force the selection to match one of the filters. + filters = {} +} + +---Add a new settings section to the settings UI +---@param section string +---@param options settings.option[] +---@param plugin_name? string Optional name of plugin +---@param overwrite? boolean Overwrite previous section options +function settings.add(section, options, plugin_name, overwrite) + local category = "" + if plugin_name ~= nil then + category = "plugins" + else + category = "core" + end + + if overwrite and settings[category][section] then + settings[category][section] = {} + end + + if not settings[category][section] then + settings[category][section] = {} + if category ~= "plugins" then + table.insert(settings.sections, section) + else + table.insert(settings.plugin_sections, section) + end + end + + if plugin_name ~= nil then + if not settings[category][section][plugin_name] then + settings[category][section][plugin_name] = {} + end + for _, option in ipairs(options) do + table.insert(settings[category][section][plugin_name], option) + end + else + for _, option in ipairs(options) do + table.insert(settings[category][section], option) + end + end +end + +-------------------------------------------------------------------------------- +-- Add Core Settings +-------------------------------------------------------------------------------- + +settings.add("General", + { + { + label = "User Module", + description = "Open your init.lua for customizations.", + type = settings.type.BUTTON, + icon = "P", + on_click = "core:open-user-module" + }, + { + label = "Clear Fonts Cache", + description = "Delete current font cache and regenerate a fresh one.", + type = settings.type.BUTTON, + icon = "C", + on_click = function() + Fonts.clean_cache() + end + }, + { + label = "Maximum Project Files", + description = "The maximum amount of project files to register.", + path = "max_project_files", + type = settings.type.NUMBER, + default = 2000, + min = 1, + max = 100000, + on_apply = function(button, x, y) + if button == "left" then + core.rescan_project_directories() + end + end + }, + { + label = "File Size Limit", + description = "The maximum file size in megabytes allowed for editing.", + path = "file_size_limit", + type = settings.type.NUMBER, + default = 10, + min = 1, + max = 50 + }, + { + label = "Ignore Files", + description = "List of lua patterns matching files to be ignored by the editor.", + path = "ignore_files", + type = settings.type.LIST_STRINGS, + default = { "^%." }, + on_apply = function() + core.rescan_project_directories() + end + }, + { + label = "Maximum Clicks", + description = "The maximum amount of consecutive clicks that are registered by the editor.", + path = "max_clicks", + type = settings.type.NUMBER, + default = 3, + min = 1, + max = 10 + }, + } +) + +settings.add("Graphics", + { + { + label = "Frames Per Second", + description = "Lower value for low end machines and higher for a smoother experience.", + path = "fps", + type = settings.type.NUMBER, + default = 60, + min = 10, + max = 300 + }, + { + label = "Transitions", + description = "If disabled turns off all transitions but improves rendering performance.", + path = "transitions", + type = settings.type.TOGGLE, + default = true + }, + { + label = "Animation Rate", + description = "The amount of time it takes for a transition to finish.", + path = "animation_rate", + type = settings.type.NUMBER, + default = 1.0, + min = 0.5, + max = 3.0, + step = 0.1 + }, + { + label = "Animate Mouse Drag Scroll", + description = "Causes higher cpu usage but smoother scroll transition.", + path = "animate_drag_scroll", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Scrolling Transitions", + path = "disabled_transitions.scroll", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Command View Transitions", + path = "disabled_transitions.commandview", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Context Menu Transitions", + path = "disabled_transitions.contextmenu", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Log View Transitions", + path = "disabled_transitions.logview", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Nag Bar Transitions", + path = "disabled_transitions.nagbar", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Tab Transitions", + path = "disabled_transitions.tabs", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Tab Drag Transitions", + path = "disabled_transitions.tab_drag", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Disable Status Bar Transitions", + path = "disabled_transitions.statusbar", + type = settings.type.TOGGLE, + default = false + }, + } +) + +settings.add("User Interface", + { + { + label = "Font", + description = "The font and fallbacks used on non code text.", + path = "font", + type = settings.type.FONT, + fonts_list = style, + default = { + fonts = { + { + name = "Fira Sans Regular", + path = DATADIR .. "/fonts/FiraSans-Regular.ttf" + } + }, + options = { + size = 15, + antialiasing = "subpixel", + hinting = "slight" + } + } + }, + { + label = "Borderless", + description = "Use built-in window decorations.", + path = "borderless", + type = settings.type.TOGGLE, + default = false, + on_apply = function() + core.configure_borderless_window() + end + }, + { + label = "Always Show Tabs", + description = "Shows tabs even if a single document is opened.", + path = "always_show_tabs", + type = settings.type.TOGGLE, + default = true + }, + { + label = "Maximum Tabs", + description = "The maximum amount of visible document tabs.", + path = "max_tabs", + type = settings.type.NUMBER, + default = 8, + min = 1, + max = 100 + }, + { + label = "Close Button on Tabs", + description = "Display the close button on tabs.", + path = "tab_close_button", + type = settings.type.TOGGLE, + default = true + }, + { + label = "Mouse wheel scroll rate", + description = "The amount to scroll when using the mouse wheel.", + path = "mouse_wheel_scroll", + type = settings.type.NUMBER, + default = 50, + min = 10, + max = 200, + get_value = function(value) + return value / SCALE + end, + set_value = function(value) + return value * SCALE + end + }, + { + label = "Disable Cursor Blinking", + description = "Disables cursor blinking on text input elements.", + path = "disable_blink", + type = settings.type.TOGGLE, + default = false + }, + { + label = "Cursor Blinking Period", + description = "Interval in seconds in which the cursor blinks.", + path = "blink_period", + type = settings.type.NUMBER, + default = 0.8, + min = 0.3, + max = 2.0, + step = 0.1 + } + } +) + +settings.add("Editor", + { + { + label = "Code Font", + description = "The font and fallbacks used on the code editor.", + path = "code_font", + type = settings.type.FONT, + fonts_list = style, + default = { + fonts = { + { + name = "JetBrains Mono Regular", + path = DATADIR .. "/fonts/JetBrainsMono-Regular.ttf" + } + }, + options = { + size = 15, + antialiasing = "subpixel", + hinting = "slight" + } + } + }, + { + label = "Indentation Type", + description = "The character inserted when pressing the tab key.", + path = "tab_type", + type = settings.type.SELECTION, + default = "soft", + values = { + {"Space", "soft"}, + {"Tab", "hard"} + } + }, + { + label = "Indentation Size", + description = "Amount of spaces shown per indentation.", + path = "indent_size", + type = settings.type.NUMBER, + default = 2, + min = 1, + max = 10 + }, + { + label = "Line Limit", + description = "Amount of characters at which the line breaking column will be drawn.", + path = "line_limit", + type = settings.type.NUMBER, + default = 80, + min = 1 + }, + { + label = "Line Height", + description = "The amount of spacing between lines.", + path = "line_height", + type = settings.type.NUMBER, + default = 1.2, + min = 1.0, + max = 3.0, + step = 0.1 + }, + { + label = "Highlight Line", + description = "Highlight the current line.", + path = "highlight_current_line", + type = settings.type.SELECTION, + default = true, + values = { + {"Yes", true}, + {"No", false}, + {"No Selection", "no_selection"} + }, + set_value = function(value) + if type(value) == "nil" then return false end + return value + end + }, + { + label = "Maximum Undo History", + description = "The amount of undo elements to keep.", + path = "max_undos", + type = settings.type.NUMBER, + default = 10000, + min = 100, + max = 100000 + }, + { + label = "Undo Merge Timeout", + description = "Time in seconds before applying an undo action.", + path = "undo_merge_timeout", + type = settings.type.NUMBER, + default = 0.3, + min = 0.1, + max = 1.0, + step = 0.1 + }, + { + label = "Symbol Pattern", + description = "A lua pattern used to match symbols in the document.", + path = "symbol_pattern", + type = settings.type.STRING, + default = "[%a_][%w_]*" + }, + { + label = "Non Word Characters", + description = "A string of characters that do not belong to a word.", + path = "non_word_chars", + type = settings.type.STRING, + default = " \\t\\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-", + get_value = function(value) + return value:gsub("\n", "\\n"):gsub("\t", "\\t") + end, + set_value = function(value) + return value:gsub("\\n", "\n"):gsub("\\t", "\t") + end + }, + { + label = "Scroll Past the End", + description = "Allow scrolling beyond the document ending.", + path = "scroll_past_end", + type = settings.type.TOGGLE, + default = true + } + } +) + +settings.add("Development", + { + { + label = "Core Log", + description = "Open the list of logged messages.", + type = settings.type.BUTTON, + icon = "f", + on_click = "core:open-log" + }, + { + label = "Log Items", + description = "The maximum amount of entries to keep on the log UI.", + path = "max_log_items", + type = settings.type.NUMBER, + default = 80, + min = 50, + max = 2000 + }, + { + label = "Skip Plugins Version", + description = "Do not verify the plugins required versions at startup.", + path = "skip_plugins_version", + type = settings.type.TOGGLE, + default = false + } + } +) + +settings.add("Status Bar", + { + { + label = "Enabled", + description = "Toggle the default visibility of the status bar.", + path = "statusbar.enabled", + type = settings.type.TOGGLE, + default = true, + on_apply = function(enabled) + if enabled then + core.status_view:show() + else + core.status_view:hide() + end + end + }, + { + label = "Show Notifications", + description = "Toggle the visibility of status messages.", + path = "statusbar.messages", + type = settings.type.TOGGLE, + default = true, + on_apply = function(enabled) + core.status_view:display_messages(enabled) + end + }, + { + label = "Messages Timeout", + description = "The amount in seconds before a notification dissapears.", + path = "message_timeout", + type = settings.type.NUMBER, + default = 5, + min = 1, + max = 30 + } + } +) + +---Retrieve from given config the associated value using the given path. +---@param conf table +---@param path string +---@param default any +---@return any | nil +local function get_config_value(conf, path, default) + local sections = {}; + for match in (path.."."):gmatch("(.-)%.") do + table.insert(sections, match); + end + + local element = conf + for _, section in ipairs(sections) do + if type(element[section]) ~= "nil" then + element = element[section] + else + return default + end + end + + if type(element) == "nil" then + return default + end + + return element +end + +---Loops the given config table using the given path and store the value. +---@param conf table +---@param path string +---@param value any +local function set_config_value(conf, path, value) + local sections = {}; + for match in (path.."."):gmatch("(.-)%.") do + table.insert(sections, match); + end + + local sections_count = #sections + + if sections_count == 1 then + conf[sections[1]] = value + return + elseif type(conf[sections[1]]) ~= "table" then + conf[sections[1]] = {} + end + + local element = conf + for idx, section in ipairs(sections) do + if type(element[section]) ~= "table" then + element[section] = {} + element = element[section] + else + element = element[section] + end + if idx + 1 == sections_count then break end + end + + element[sections[sections_count]] = value +end + +---Get a list of system and user installed plugins. +---@return table<integer, string> +local function get_installed_plugins() + local files, ordered = {}, {} + + for _, root_dir in ipairs {DATADIR, USERDIR} do + local plugin_dir = root_dir .. "/plugins" + for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do + local valid = false + local file_info = system.get_file_info(plugin_dir .. "/" .. filename) + if file_info then + if + file_info.type == "file" + and + filename:match("%.lua$") + and + not filename:match("^language_") + then + valid = true + filename = filename:gsub("%.lua$", "") + elseif file_info.type == "dir" then + if system.get_file_info(plugin_dir .. "/" .. filename .. "/init.lua") then + valid = true + end + end + end + if valid then + if not files[filename] then table.insert(ordered, filename) end + files[filename] = true + end + end + end + + table.sort(ordered) + + return ordered +end + +---Get a list of system and user installed colors. +---@return table<integer, table> +local function get_installed_colors() + local files, ordered = {}, {} + + for _, root_dir in ipairs {DATADIR, USERDIR} do + local dir = root_dir .. "/colors" + for _, filename in ipairs(system.list_dir(dir) or {}) do + local file_info = system.get_file_info(dir .. "/" .. filename) + if + file_info and file_info.type == "file" + and + filename:match("%.lua$") + then + -- read colors + local contents = io.open(dir .. "/" .. filename):read("*a") + local colors = {} + for r, g, b in contents:gmatch("#(%x%x)(%x%x)(%x%x)") do + r = tonumber(r, 16) + g = tonumber(g, 16) + b = tonumber(b, 16) + table.insert(colors, { r, g, b, 0xff }) + end + -- sort colors from darker to lighter + table.sort(colors, function(a, b) + return a[1] + a[2] + a[3] < b[1] + b[2] + b[3] + end) + -- remove duplicate colors + local b = {} + for i = #colors, 1, -1 do + local a = colors[i] + if a[1] == b[1] and a[2] == b[2] and a[3] == b[3] then + table.remove(colors, i) + else + b = colors[i] + end + end + -- insert color to ordered table if not duplicate + filename = filename:gsub("%.lua$", "") + if not files[filename] then + table.insert(ordered, {name = filename, colors = colors}) + end + files[filename] = true + end + end + end + + table.sort(ordered, function(a, b) return a.name < b.name end) + + return ordered +end + +---Capitalize first letter of every word. +---Taken from core.command. +---@param words string +---@return string +local function capitalize_first(words) + return words:sub(1, 1):upper() .. words:sub(2) +end + +---Similar to command prettify_name but also takes care of underscores. +---@param name string +---@return string +local function prettify_name(name) + name = name:gsub("[%-_]", " "):gsub("%S+", capitalize_first) + return name +end + +---Load config options from the USERDIR user_settings.lua and store them on +---settings.config for later usage. +local function load_settings() + local ok, t = pcall(dofile, USERDIR .. "/user_settings.lua") + settings.config = ok and t.config or {} +end + +---Save current config options into the USERDIR user_settings.lua +local function save_settings() + local fp = io.open(USERDIR .. "/user_settings.lua", "w") + if fp then + local output = "{\n [\"config\"] = " + .. common.serialize( + settings.config, + { pretty = true, escape = true, sort = true, initial_indent = 1 } + ):gsub("^%s+", "") + .. "\n}\n" + fp:write("return ", output) + fp:close() + end +end + +---Apply a keybinding and optionally save it. +---@param cmd string +---@param bindings table<integer, string> +---@param skip_save? boolean +---@return table | nil +local function apply_keybinding(cmd, bindings, skip_save) + local row_value = nil + local changed = false + + local original_bindings = { keymap.get_binding(cmd) } + for _, binding in ipairs(original_bindings) do + keymap.unbind(binding, cmd) + end + + if #bindings > 0 then + if + not skip_save + and + settings.config.custom_keybindings + and + settings.config.custom_keybindings[cmd] + then + settings.config.custom_keybindings[cmd] = {} + end + local shortcuts = "" + for _, binding in ipairs(bindings) do + if not binding:match("%+$") and binding ~= "" and binding ~= "none" then + keymap.add({[binding] = cmd}) + shortcuts = shortcuts .. binding .. "\n" + if not skip_save then + if not settings.config.custom_keybindings then + settings.config.custom_keybindings = {} + settings.config.custom_keybindings[cmd] = {} + elseif not settings.config.custom_keybindings[cmd] then + settings.config.custom_keybindings[cmd] = {} + end + table.insert(settings.config.custom_keybindings[cmd], binding) + changed = true + end + end + end + if shortcuts ~= "" then + local bindings_list = shortcuts:gsub("\n$", "") + row_value = { + style.text, cmd, ListBox.COLEND, style.dim, bindings_list + } + end + elseif + not skip_save + and + settings.config.custom_keybindings + and + settings.config.custom_keybindings[cmd] + then + settings.config.custom_keybindings[cmd] = nil + changed = true + end + + if changed then + save_settings() + end + + if not row_value then + row_value = { + style.text, cmd, ListBox.COLEND, style.dim, "none" + } + end + + return row_value +end + +---Load the saved fonts into the config path or fonts_list table. +---@param option settings.option +---@param path string +---@param saved_value any +local function merge_font_settings(option, path, saved_value) + local font_options = saved_value.options or { + size = 15, + antialiasing = "supixel", + hinting = "slight" + } + font_options.size = font_options.size or 15 + font_options.antialiasing = font_options.antialiasing or "subpixel" + font_options.hinting = font_options.hinting or "slight" + + local fonts = {} + local font_loaded = true + for _, font in ipairs(saved_value.fonts) do + local font_data = nil + font_loaded = core.try(function() + font_data = renderer.font.load( + font.path, font_options.size * SCALE, font_options + ) + end) + if font_loaded then + table.insert(fonts, font_data) + else + option.font_error = true + core.error("Settings: could not load %s\n'%s - %s'", path, font.name, font.path) + break + end + end + + if font_loaded then + if option.fonts_list then + set_config_value(option.fonts_list, option.path, renderer.font.group(fonts)) + else + set_config_value(config, path, renderer.font.group(fonts)) + end + end +end + +---Merge previously saved settings without destroying the config table. +local function merge_settings() + if type(settings.config) ~= "table" then return end + + -- merge core settings + for _, section in ipairs(settings.sections) do + local options = settings.core[section] + for _, option in ipairs(options) do + if type(option.path) == "string" then + local saved_value = get_config_value(settings.config, option.path) + if type(saved_value) ~= "nil" then + if option.type == settings.type.FONT or option.type == "font" then + merge_font_settings(option, option.path, saved_value) + else + set_config_value(config, option.path, saved_value) + end + if option.on_apply then + option.on_apply(saved_value) + end + end + end + end + end + + -- merge plugin settings + table.sort(settings.plugin_sections) + for _, section in ipairs(settings.plugin_sections) do + local plugins = settings.plugins[section] + for plugin_name, options in pairs(plugins) do + for _, option in pairs(options) do + if type(option.path) == "string" then + local path = "plugins." .. plugin_name .. "." .. option.path + local saved_value = get_config_value(settings.config, path) + if type(saved_value) ~= "nil" then + if option.type == settings.type.FONT or option.type == "font" then + merge_font_settings(option, path, saved_value) + else + set_config_value(config, path, saved_value) + end + if option.on_apply then + option.on_apply(saved_value) + end + end + end + end + end + end + + -- apply custom keybindings + if settings.config.custom_keybindings then + for cmd, bindings in pairs(settings.config.custom_keybindings) do + apply_keybinding(cmd, bindings, true) + end + end +end + +---Scan all plugins to check if they define a config_spec and load it. +local function scan_plugins_spec() + for plugin, conf in pairs(config.plugins) do + if type(conf) == "table" and conf.config_spec then + settings.add( + conf.config_spec.name, + conf.config_spec, + plugin + ) + end + end +end + +---Called at core first run to store the default keybindings. +local function store_default_keybindings() + for name, _ in pairs(command.map) do + local keys = { keymap.get_binding(name) } + if #keys > 0 then + settings.default_keybindings[name] = keys + end + end +end + +---@class settings.ui : widget +---@field private notebook widget.notebook +---@field private core widget +---@field private colors widget +---@field private plugins widget +---@field private keybinds widget +---@field private about widget +---@field private core_sections widget.foldingbook +---@field private plugin_sections widget.foldingbook +local Settings = Widget:extend() + +---Constructor +function Settings:new() + Settings.super.new(self, nil, false) + + self.name = "Settings" + self.defer_draw = false + self.border.width = 0 + self.draggable = false + self.scrollable = false + + ---@type widget.notebook + self.notebook = NoteBook(self) + self.notebook.size.x = 250 + self.notebook.size.y = 300 + self.notebook.border.width = 0 + + self.core = self.notebook:add_pane("core", "Core") + self.colors = self.notebook:add_pane("colors", "Colors") + self.plugins = self.notebook:add_pane("plugins", "Plugins") + self.keybinds = self.notebook:add_pane("keybindings", "Keybindings") + self.about = self.notebook:add_pane("about", "About") + + self.notebook:set_pane_icon("core", "P") + self.notebook:set_pane_icon("colors", "W") + self.notebook:set_pane_icon("plugins", "B") + self.notebook:set_pane_icon("keybindings", "M") + self.notebook:set_pane_icon("about", "i") + + self.core_sections = FoldingBook(self.core) + self.core_sections.border.width = 0 + self.core_sections.scrollable = false + + self.plugin_sections = FoldingBook(self.plugins) + self.plugin_sections.border.width = 0 + self.plugin_sections.scrollable = false + + self:load_core_settings() + self:load_color_settings() + self:load_plugin_settings() + self:load_keymap_settings() + + self:setup_about() +end + +---Helper function to add control for both core and plugin settings. +---@oaram pane widget +---@param option settings.option +---@param plugin_name? string | nil +local function add_control(pane, option, plugin_name) + local found = false + local path = type(plugin_name) ~= "nil" and + "plugins." .. plugin_name .. "." .. option.path or option.path + local option_value = nil + if type(path) ~= "nil" then + option_value = get_config_value(config, path, option.default) + end + + if option.get_value then + option_value = option.get_value(option_value) + end + + ---@type widget + local widget = nil + + if type(option.type) == "string" then + option.type = settings.type[option.type:upper()] + end + + if option.type == settings.type.NUMBER then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.numberbox + local number = NumberBox(pane, option_value, option.min, option.max, option.step) + widget = number + found = true + + elseif option.type == settings.type.TOGGLE then + ---@type widget.toggle + local toggle = Toggle(pane, option.label, option_value) + widget = toggle + found = true + + elseif option.type == settings.type.STRING then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.textbox + local string = TextBox(pane, option_value or "") + widget = string + found = true + + elseif option.type == settings.type.SELECTION then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.selectbox + local select = SelectBox(pane) + for _, data in pairs(option.values) do + select:add_option(data[1], data[2]) + end + for idx, _ in ipairs(select.list.rows) do + if select.list:get_row_data(idx) == option_value then + select:set_selected(idx-1) + break + end + end + widget = select + found = true + + elseif option.type == settings.type.BUTTON then + ---@type widget.button + local button = Button(pane, option.label) + if option.icon then + button:set_icon(option.icon) + end + if option.on_click then + local command_type = type(option.on_click) + if command_type == "string" then + function button:on_click() + command.perform(option.on_click) + end + elseif command_type == "function" then + button.on_click = option.on_click + end + end + widget = button + found = true + + elseif option.type == settings.type.LIST_STRINGS then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.itemslist + local list = ItemsList(pane) + if type(option_value) == "table" then + for _, value in ipairs(option_value) do + list:add_item(value) + end + end + widget = list + found = true + + elseif option.type == settings.type.FONT then + --get fonts without conversion to renderer.font + if type(path) ~= "nil" then + if not option.font_error then + option_value = get_config_value(settings.config, path, option.default) + else + --fallback to default fonts if error loading user defined ones + option_value = option.default + end + end + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.fontslist + local fonts = FontsList(pane) + if type(option_value) == "table" then + for _, font in ipairs(option_value.fonts) do + fonts:add_font(font) + end + + local font_options = option_value.options or { + size = 15, + antialiasing = "supixel", + hinting = "slight" + } + font_options.size = font_options.size or 15 + font_options.antialiasing = font_options.antialiasing or "subpixel" + font_options.hinting = font_options.hinting or "slight" + fonts:set_options(font_options) + end + widget = fonts + found = true + + elseif option.type == settings.type.FILE then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.filepicker + local file = FilePicker(pane, option_value or "") + if option.exists then + file:set_mode(FilePicker.mode.FILE_EXISTS) + else + file:set_mode(FilePicker.mode.FILE) + end + file.filters = option.filters or {} + widget = file + found = true + + elseif option.type == settings.type.DIRECTORY then + ---@type widget.label + Label(pane, option.label .. ":") + ---@type widget.filepicker + local file = FilePicker(pane, option_value or "") + if option.exists then + file:set_mode(FilePicker.mode.DIRECTORY_EXISTS) + else + file:set_mode(FilePicker.mode.DIRECTORY) + end + file.filters = option.filters or {} + widget = file + found = true + end + + if widget and type(path) ~= "nil" then + function widget:on_change(value) + if self:is(SelectBox) then + value = self:get_selected_data() + elseif self:is(ItemsList) then + value = self:get_items() + elseif self:is(FontsList) then + value = { + fonts = self:get_fonts(), + options = self:get_options() + } + end + + if option.set_value then + value = option.set_value(value) + end + + if self:is(FontsList) then + local fonts = {} + for _, font in ipairs(value.fonts) do + table.insert(fonts, renderer.font.load( + font.path, value.options.size * SCALE, value.options + )) + end + if option.fonts_list then + set_config_value(option.fonts_list, path, renderer.font.group(fonts)) + else + set_config_value(config, path, renderer.font.group(fonts)) + end + else + set_config_value(config, path, value) + end + + set_config_value(settings.config, path, value) + save_settings() + if option.on_apply then + option.on_apply(value) + end + end + end + + if (option.description or option.default) and found then + local text = option.description or "" + local default = "" + local default_type = type(option.default) + if default_type ~= "table" and default_type ~= "nil" then + if text ~= "" then + text = text .. " " + end + default = string.format("(default: %s)", option.default) + end + ---@type widget.label + local description = Label(pane, text .. default) + description.desc = true + end +end + +---Generate all the widgets for core settings. +function Settings:load_core_settings() + for _, section in ipairs(settings.sections) do + local options = settings.core[section] + + ---@type widget|widget.foldingbook.pane + local pane = self.core_sections:get_pane(section) + if not pane then + pane = self.core_sections:add_pane(section, section) + else + pane = pane.container + end + + for _, opt in ipairs(options) do + ---@type settings.option + local option = opt + add_control(pane, option) + end + end +end + +---Function in charge of rendering the colors column of the color pane. +---@param self widget.listbox +---@oaram row integer +---@param x integer +---@param y integer +---@param font renderer.font +---@param color renderer.color +---@param only_calc boolean +---@return number width +---@return number height +local function on_color_draw(self, row, x, y, font, color, only_calc) + local w = self:get_width() - (x - self.position.x) - style.padding.x + local h = font:get_height() + + if not only_calc then + local row_data = self:get_row_data(row) + local width = w/#row_data.colors + + for i = 1, #row_data.colors do + renderer.draw_rect(x + ((i - 1) * width), y, width, h, row_data.colors[i]) + end + end + + return w, h +end + +---Generate the list of all available colors with preview +function Settings:load_color_settings() + self.colors.scrollable = false + + local colors = get_installed_colors() + + ---@type widget.listbox + local listbox = ListBox(self.colors) + + listbox.border.width = 0 + listbox:enable_expand(true) + + listbox:add_column("Theme") + listbox:add_column("Colors") + + for idx, details in ipairs(colors) do + local name = details.name + if settings.config.theme and settings.config.theme == name then + listbox:set_selected(idx) + end + listbox:add_row({ + style.text, name, ListBox.COLEND, on_color_draw + }, {name = name, colors = details.colors}) + end + + function listbox:on_row_click(idx, data) + core.reload_module("colors." .. data.name) + settings.config.theme = data.name + save_settings() + end +end + +---Unload a plugin settings from plugins section. +---@param plugin string +function Settings:disable_plugin(plugin) + for _, section in ipairs(settings.plugin_sections) do + local plugins = settings.plugins[section] + + for plugin_name, options in pairs(plugins) do + if plugin_name == plugin then + self.plugin_sections:delete_pane(section) + end + end + end + + if + type(settings.config.enabled_plugins) == "table" + and + settings.config.enabled_plugins[plugin] + then + settings.config.enabled_plugins[plugin] = nil + end + if type(settings.config.disabled_plugins) ~= "table" then + settings.config.disabled_plugins = {} + end + + settings.config.disabled_plugins[plugin] = true + save_settings() +end + +---Load plugin and append its settings to the plugins section. +---@param plugin string +function Settings:enable_plugin(plugin) + local loaded = false + local config_type = type(config.plugins[plugin]) + if config_type == "boolean" or config_type == "nil" then + config.plugins[plugin] = {} + loaded = true + end + + require("plugins." .. plugin) + + if config.plugins[plugin] and config.plugins[plugin].config_spec then + local conf = config.plugins[plugin].config_spec + settings.add(conf.name, conf, plugin, true) + end + + for _, section in ipairs(settings.plugin_sections) do + local plugins = settings.plugins[section] + + for plugin_name, options in pairs(plugins) do + if plugin_name == plugin then + ---@type widget + local pane = self.plugin_sections:get_pane(section) + if not pane then + pane = self.plugin_sections:add_pane(section, section) + else + pane = pane.container + end + + for _, opt in ipairs(options) do + ---@type settings.option + local option = opt + add_control(pane, option, plugin_name) + end + end + end + end + + if + type(settings.config.disabled_plugins) == "table" + and + settings.config.disabled_plugins[plugin] + then + settings.config.disabled_plugins[plugin] = nil + end + if type(settings.config.enabled_plugins) ~= "table" then + settings.config.enabled_plugins = {} + end + + settings.config.enabled_plugins[plugin] = true + save_settings() + + if loaded then + core.log("Loaded '%s' plugin", plugin) + end +end + +---Generate all the widgets for plugin settings. +function Settings:load_plugin_settings() + ---@type widget + local pane = self.plugin_sections:get_pane("enable_disable") + if not pane then + pane = self.plugin_sections:add_pane("enable_disable", "Installed") + else + pane = pane.container + end + + -- requires earlier access to startup process + Label( + pane, + "Notice: disabling plugins will not take effect until next restart" + ) + + Line(pane, 2, 10) + + local plugins = get_installed_plugins() + for _, plugin in ipairs(plugins) do + if plugin ~= "settings" then + local enabled = false + + if + ( + type(config.plugins[plugin]) ~= "nil" + and + config.plugins[plugin] ~= false + ) + or + ( + settings.config.enabled_plugins + and + settings.config.enabled_plugins[plugin] + ) + then + enabled = true + end + + local this = self + + ---@type widget.toggle + local toggle = Toggle(pane, prettify_name(plugin), enabled) + function toggle:on_change(value) + if value then + this:enable_plugin(plugin) + else + this:disable_plugin(plugin) + end + end + end + end + + table.sort(settings.plugin_sections) + + for _, section in ipairs(settings.plugin_sections) do + local plugins = settings.plugins[section] + + for plugin_name, options in pairs(plugins) do + ---@type widget + local pane = self.plugin_sections:get_pane(section) + if not pane then + pane = self.plugin_sections:add_pane(section, section) + else + pane = pane.container + end + + for _, opt in ipairs(options) do + ---@type settings.option + local option = opt + add_control(pane, option, plugin_name) + end + end + end +end + +---@type widget.keybinddialog +local keymap_dialog = KeybindingDialog() + +function keymap_dialog:on_save(bindings) + local row_value = apply_keybinding(self.command, bindings) + if row_value then + self.listbox:set_row(self.row_id, row_value) + end +end + +function keymap_dialog:on_reset() + local default_keys = settings.default_keybindings[self.command] + local current_keys = { keymap.get_binding(self.command) } + + for _, binding in ipairs(current_keys) do + keymap.unbind(binding, self.command) + end + + if default_keys and #default_keys > 0 then + local cmd = self.command + if not settings.config.custom_keybindings then + settings.config.custom_keybindings = {} + settings.config.custom_keybindings[cmd] = {} + elseif not settings.config.custom_keybindings[cmd] then + settings.config.custom_keybindings[cmd] = {} + end + local shortcuts = "" + for _, binding in ipairs(default_keys) do + keymap.add({[binding] = cmd}) + shortcuts = shortcuts .. binding .. "\n" + table.insert(settings.config.custom_keybindings[cmd], binding) + end + local bindings_list = shortcuts:gsub("\n$", "") + self.listbox:set_row(self.row_id, { + style.text, cmd, ListBox.COLEND, style.dim, bindings_list + }) + else + self.listbox:set_row(self.row_id, { + style.text, self.command, ListBox.COLEND, style.dim, "none" + }) + end + if + settings.config.custom_keybindings + and + settings.config.custom_keybindings[self.command] + then + settings.config.custom_keybindings[self.command] = nil + save_settings() + end +end + +---Generate the list of all available commands and allow editing their keymaps. +function Settings:load_keymap_settings() + self.keybinds.scrollable = false + + local ordered = {} + for name, _ in pairs(command.map) do + table.insert(ordered, name) + end + table.sort(ordered) + + ---@type widget.listbox + local listbox = ListBox(self.keybinds) + + listbox.border.width = 0 + listbox:enable_expand(true) + + listbox:add_column("Command") + listbox:add_column("Bindings") + + for _, name in ipairs(ordered) do + local keys = { keymap.get_binding(name) } + local binding = "" + if #keys == 1 then + binding = keys[1] + elseif #keys > 1 then + binding = keys[1] + for idx, key in ipairs(keys) do + if idx ~= 1 then + binding = binding .. "\n" .. key + end + end + elseif #keys < 1 then + binding = "none" + end + listbox:add_row({ + style.text, name, ListBox.COLEND, style.dim, binding + }, name) + end + + function listbox:on_row_click(idx, data) + if not keymap_dialog:is_visible() then + local bindings = { keymap.get_binding(data) } + keymap_dialog:set_bindings(bindings) + keymap_dialog.row_id = idx + keymap_dialog.command = data + keymap_dialog.listbox = self + keymap_dialog:show() + end + end +end + +function Settings:setup_about() + ---@type widget.label + local title = Label(self.about, "Lite XL") + title.font = "big_font" + ---@type widget.label + local version = Label(self.about, "version " .. VERSION) + ---@type widget.label + local description = Label( + self.about, + "A lightweight text editor written in Lua, adapted from lite." + ) + + local function open_link(link) + local platform_filelauncher + if PLATFORM == "Windows" then + platform_filelauncher = "start" + elseif PLATFORM == "Mac OS X" then + platform_filelauncher = "open" + else + platform_filelauncher = "xdg-open" + end + system.exec(platform_filelauncher .. " " .. link) + end + + ---@type widget.button + local button = Button(self.about, "Visit Website") + button:set_tooltip("Open https://lite-xl.com/") + function button:on_click() open_link("https://lite-xl.com/") end + + ---@type widget.listbox + local contributors = ListBox(self.about) + contributors.scrollable = true + contributors:add_column("Contributors") + contributors:add_column("") + contributors:add_column("Website") + function contributors:on_row_click(_, data) open_link(data) end + +local contributors_list = { + { "Rxi", "Lite Founder", "https://github.com/rxi" }, + { "Francesco Abbate", "Lite XL Founder", "https://github.com/franko" }, + { "Adam Harrison", "Core", "https://github.com/adamharrison" }, + { "Andrea Zanellato", "CI, Website", "https://github.com/redtide" }, + { "Björn Buckwalter", "MacOS Support", "https://github.com/bjornbm" }, + { "boppyt", "Contributor", "https://github.com/boppyt" }, + { "Cukmekerb", "Contributor", "https://github.com/vincens2005" }, + { "Daniel Rocha", "Contributor", "https://github.com/dannRocha" }, + { "daubaris", "Contributor", "https://github.com/daubaris" }, + { "Dheisom Gomes", "Contributor", "https://github.com/dheisom" }, + { "Evgeny Petrovskiy", "Contributor", "https://github.com/eugenpt" }, + { "Ferdinand Prantl", "Contributor", "https://github.com/prantlf" }, + { "Jan", "Build System", "https://github.com/Jan200101" }, + { "Janis-Leuenberger", "MacOS Support", "https://github.com/Janis-Leuenberger" }, + { "Jefferson", "Contributor", "https://github.com/jgmdev" }, + { "Jipok", "Contributor", "https://github.com/Jipok" }, + { "Joshua Minor", "Contributor", "https://github.com/jminor" }, + { "George Linkovsky", "Contributor", "https://github.com/Timofffee" }, + { "Guldoman", "Core", "https://github.com/Guldoman" }, + { "liquidev", "Contributor", "https://github.com/liquidev" }, + { "Mat Mariani", "MacOS Support", "https://github.com/mathewmariani" }, + { "Nightwing", "Contributor", "https://github.com/Nightwing13" }, + { "Nils Kvist", "Contributor", "https://github.com/budRich" }, + { "Not-a-web-Developer", "Contributor", "https://github.com/Not-a-web-Developer" }, + { "Robert Štojs", "CI", "https://github.com/netrobert" }, + { "sammyette", "Plugins", "https://github.com/TorchedSammy" }, + { "Takase", "Core", "https://github.com/takase1121" }, + { "xwii", "Contributor", "https://github.com/xcb-xwii" } +} + + for _, c in ipairs(contributors_list) do + contributors:add_row({ + c[1], ListBox.COLEND, c[2], ListBox.COLEND, c[3] + }, c[3]) + end + + ---@param self widget + function self.about:update_positions() + local center = self:get_width() / 2 + + title:set_label("Lite XL") + title:set_position( + center - (title:get_width() / 2), + style.padding.y + ) + + version:set_position( + center - (version:get_width() / 2), + title:get_bottom() + (style.padding.y / 2) + ) + + description:set_position( + center - (description:get_width() / 2), + version:get_bottom() + (style.padding.y / 2) + ) + + button:set_position( + center - (button:get_width() / 2), + description:get_bottom() + style.padding.y + ) + + contributors:set_position( + style.padding.x, + button:get_bottom() + style.padding.y + ) + + contributors:set_size( + self:get_width() - (style.padding.x * 2), + self:get_height() - (button:get_bottom() + (style.padding.y * 2)) + ) + + contributors:set_visible_rows() + end +end + +---Reposition and resize core and plugin widgets. +function Settings:update() + if not Settings.super.update(self) then return end + + self.notebook:set_size(self.size.x, self.size.y) + + for _, section in ipairs({self.core_sections, self.plugin_sections}) do + if section.parent:is_visible() then + section:set_size( + section.parent.size.x - (style.padding.x), + section:get_real_height() + ) + section:set_position(style.padding.x / 2, 0) + for _, pane in ipairs(section.panes) do + local prev_child = nil + for pos=#pane.container.childs, 1, -1 do + local child = pane.container.childs[pos] + local x, y = 10, (10 * SCALE) + if prev_child then + if + (prev_child:is(Label) and not prev_child.desc) + or + (child:is(Label) and child.desc) + then + y = prev_child:get_bottom() + (10 * SCALE) + else + y = prev_child:get_bottom() + (30 * SCALE) + end + end + if child:is(Line) then + x = 0 + elseif child:is(ItemsList) or child:is(FilePicker) or child:is(TextBox) then + child:set_size(pane.container:get_width() - 20, child.size.y) + end + child:set_position(x, y) + prev_child = child + end + end + end + end + + if self.about:is_visible() then + self.about:update_positions() + end +end + +-------------------------------------------------------------------------------- +-- overwrite core run to inject previously saved settings +-------------------------------------------------------------------------------- +local core_run = core.run +function core.run() + store_default_keybindings() + + -- load plugins disabled by default and enabled by user + if settings.config.enabled_plugins then + for name, _ in pairs(settings.config.enabled_plugins) do + if + type(config.plugins[name]) == "boolean" + and + not config.plugins[name] + then + require("plugins." .. name) + end + end + end + + -- append all settings defined in the plugins spec + scan_plugins_spec() + + -- merge custom settings into config + merge_settings() + + ---@type settings.ui + settings.ui = Settings() + + -- apply user chosen color theme + if settings.config.theme and settings.config.theme ~= "default" then + core.try(function() + core.reload_module("colors." .. settings.config.theme) + end) + end + + -- re-apply user settings + core.load_user_directory() + core.load_project_module() + + core_run() +end + +-------------------------------------------------------------------------------- +-- Disable plugins at startup, only works if this file is the first +-- required on user module, or priority tag is obeyed by lite-xl. +-------------------------------------------------------------------------------- +-- load custom user settings that include list of disabled plugins +load_settings() + +-- only disable non already loaded plugins +if settings.config.disabled_plugins then + for name, _ in pairs(settings.config.disabled_plugins) do + if type(rawget(config.plugins, name)) == "nil" then + config.plugins[name] = false + end + end +end + +-- properly apply skip_plugins_version before other plugins are loaded +if settings.config.skip_plugins_version then + config.skip_plugins_version = true +else + config.skip_plugins_version = false +end + +-------------------------------------------------------------------------------- +-- Add command and keymap to load settings view +-------------------------------------------------------------------------------- +command.add(nil, { + ["ui:settings"] = function() + settings.ui:show() + local node = core.root_view:get_active_node_default() + local found = false + for _, view in ipairs(node.views) do + if view == settings.ui then + found = true + node:set_active_view(view) + break + end + end + if not found then + node:add_view(settings.ui) + end + end, +}) + +keymap.add { + ["ctrl+alt+p"] = "ui:settings" +} + +-------------------------------------------------------------------------------- +-- Overwrite toolbar preferences command to open the settings gui +-------------------------------------------------------------------------------- +if config.plugins.toolbarview ~= false then + local ToolbarView = require "plugins.toolbarview" + local toolbarview_on_mouse_moved = ToolbarView.on_mouse_moved + function ToolbarView:on_mouse_moved(px, py, ...) + toolbarview_on_mouse_moved(self, px, py, ...) + if + self.hovered_item + and + self.hovered_item.command == "core:open-user-module" + then + self.hovered_item.command = "ui:settings" + end + end +end + + +return settings; diff --git a/plugins/smallclock.lua b/plugins/smallclock.lua index f656a8f..e975152 100644 --- a/plugins/smallclock.lua +++ b/plugins/smallclock.lua @@ -1,27 +1,66 @@ --- mod-version:2 -- lite-xl 2.00 +-- mod-version:3 local core = require "core" +local config = require "core.config" +local common = require "core.common" local style = require "core.style" -local status_view = require "core.statusview" +local StatusView = require "core.statusview" -local time = "" - -core.add_thread(function() - while true do - local t = os.date("*t") - time = string.format("%02d:%02d", t.hour, t.min) - coroutine.yield(1) - end -end) - -local get_items = status_view.get_items +config.plugins.smallclock = common.merge({ + enabled = true, + clock_type = "24", + -- The config specification used by the settings gui + config_spec = { + name = "Small Clock", + { + label = "Enabled", + description = "Show or hide the small clock from the status bar.", + path = "enabled", + type = "toggle", + default = true, + on_apply = function(enabled) + core.add_thread(function() + if enabled then + core.status_view:get_item("status:small-clock"):show() + else + core.status_view:get_item("status:small-clock"):hide() + end + end) + end + }, + { + label = "Clock Type", + description = "Choose between 12 or 24 hours clock mode.", + path = "clock_type", + type = "selection", + default = "24", + values = { + {"24 Hours", "24"}, + {"12 Hours", "12"} + } + } + } +}, config.plugins.smallclock) -function status_view:get_items() - local left, right = get_items(self) - local t = {style.dim, self.separator2, style.accent, time} +local time = "" - for _, item in ipairs(t) do - table.insert(right, item) +local last_time = os.time() +local function update_time() + if os.time() > last_time then + local h = config.plugins.smallclock.clock_type == "24" + and os.date("%H") or os.date("%I") + local m = os.date("%M") + time = string.format("%02d:%02d", h, m) + last_time = os.time() end - - return left, right end + +core.status_view:add_item({ + name = "status:small-clock", + alignment = StatusView.Item.RIGHT, + get_item = function() + update_time() + return {style.accent, time} + end, + position = -1, + separator = core.status_view.separator2 +}) diff --git a/plugins/smoothcaret.lua b/plugins/smoothcaret.lua index 40d852e..5639ff2 100644 --- a/plugins/smoothcaret.lua +++ b/plugins/smoothcaret.lua @@ -1,15 +1,42 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local style = require "core.style" +local common = require "core.common" local DocView = require "core.docview" -config.plugins.smoothcaret = { rate = 0.65 } +config.plugins.smoothcaret = common.merge({ + enabled = true, + rate = 0.65, + -- The config specification used by the settings gui + config_spec = { + name = "Smooth Caret", + { + label = "Enabled", + description = "Disable or enable the smooth caret animation.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Rate", + description = "Speed of the animation.", + path = "rate", + type = "number", + default = 0.65, + min = 0.2, + max = 1.0, + step = 0.05 + }, + } +}, config.plugins.smoothcaret) local docview_update = DocView.update function DocView:update() docview_update(self) + if not config.plugins.smoothcaret.enabled then return end + local minline, maxline = self:get_visible_line_range() -- We need to keep track of all the carets @@ -21,10 +48,10 @@ function DocView:update() local idx, v_idx = 1, 1 for _, line, col in self.doc:get_selections() do - local x, y = self:get_line_screen_position(line) + local x, y = self:get_line_screen_position(line, col) -- Keep the position relative to the whole View -- This way scrolling won't animate the caret - x = x + self:get_col_x_offset(line, col) + self.scroll.x + x = x + self.scroll.x y = y + self.scroll.y if not self.carets[idx] then @@ -56,7 +83,7 @@ function DocView:update() -- Remove unused carets to avoid animating new ones when they are added for i = idx, #self.carets do - self.carets[idx] = nil + self.carets[i] = nil end if self.mouse_selecting ~= self.last_mouse_selecting then @@ -72,7 +99,13 @@ function DocView:update() self.caret_idx = 1 end +local docview_draw_caret = DocView.draw_caret function DocView:draw_caret(x, y) + if not config.plugins.smoothcaret.enabled then + docview_draw_caret(self, x, y) + return + end + local c = self.visible_carets[self.caret_idx] or { current = { x = x, y = y } } local lh = self:get_line_height() diff --git a/plugins/sort.lua b/plugins/sort.lua index 6c65149..1ad4034 100644 --- a/plugins/sort.lua +++ b/plugins/sort.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local translate = require "core.doc.translate" diff --git a/plugins/spellcheck.lua b/plugins/spellcheck.lua index 7b0ba2b..f55cf1f 100644 --- a/plugins/spellcheck.lua +++ b/plugins/spellcheck.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local style = require "core.style" local config = require "core.config" @@ -7,33 +7,42 @@ local common = require "core.common" local DocView = require "core.docview" local Doc = require "core.doc" -config.plugins.spellcheck = {} -config.plugins.spellcheck.files = { "%.txt$", "%.md$", "%.markdown$" } +local platform_dictionary_file if PLATFORM == "Windows" then - config.plugins.spellcheck.dictionary_file = EXEDIR .. "/words.txt" + platform_dictionary_file = EXEDIR .. "/words.txt" else - config.plugins.spellcheck.dictionary_file = "/usr/share/dict/words" + platform_dictionary_file = "/usr/share/dict/words" end +config.plugins.spellcheck = common.merge({ + enabled = true, + files = { "%.txt$", "%.md$", "%.markdown$" }, + dictionary_file = platform_dictionary_file +}, config.plugins.spellcheck) local last_input_time = 0 local word_pattern = "%a+" local words -core.add_thread(function() - local t = {} - local i = 0 - for line in io.lines(config.plugins.spellcheck.dictionary_file) do - for word in line:gmatch(word_pattern) do - t[word:lower()] = true +local function load_dictionary() + core.add_thread(function() + local t = {} + local i = 0 + for line in io.lines(config.plugins.spellcheck.dictionary_file) do + for word in line:gmatch(word_pattern) do + t[word:lower()] = true + end + i = i + 1 + if i % 1000 == 0 then coroutine.yield() end end - i = i + 1 - if i % 1000 == 0 then coroutine.yield() end - end - words = t - core.redraw = true - core.log_quiet("Finished loading dictionary file: \"%s\"", config.plugins.spellcheck.dictionary_file) -end) + words = t + core.redraw = true + core.log_quiet( + "Finished loading dictionary file: \"%s\"", + config.plugins.spellcheck.dictionary_file + ) + end) +end local function matches_any(filename, ptns) @@ -62,11 +71,16 @@ end local draw_line_text = DocView.draw_line_text function DocView:draw_line_text(idx, x, y) - draw_line_text(self, idx, x, y) - - if not words - or not matches_any(self.doc.filename or "", config.plugins.spellcheck.files) then - return + local lh = draw_line_text(self, idx, x, y) + + if + not config.plugins.spellcheck.enabled + or + not words + or + not matches_any(self.doc.filename or "", config.plugins.spellcheck.files) + then + return lh end local s, e = 0, 0 @@ -78,12 +92,13 @@ function DocView:draw_line_text(idx, x, y) local word = text:sub(s, e):lower() if not words[word] and not active_word(self.doc, idx, e + 1) then local color = style.spellcheck_error or style.syntax.keyword2 - local x1 = x + self:get_col_x_offset(idx, s) - local x2 = x + self:get_col_x_offset(idx, e + 1) + local x1, y1 = self:get_line_screen_position(idx, s) + local x2, y2 = self:get_line_screen_position(idx, e + 1) local h = math.ceil(1 * SCALE) - renderer.draw_rect(x1, y + self:get_line_height() - h, x2 - x1, h, color) + renderer.draw_rect(x1, y1 + self:get_line_height() - h, x2 - x1, h, color) end end + return lh end @@ -112,8 +127,44 @@ local function compare_words(word1, word2) end +-- The config specification used by the settings gui +config.plugins.spellcheck.config_spec = { + name = "Spell Check", + { + label = "Enabled", + description = "Disable or enable spell checking.", + path = "enabled", + type = "toggle", + default = true + }, + { + label = "Files", + description = "List of Lua patterns matching files to spell check.", + path = "files", + type = "list_strings", + default = { "%.txt$", "%.md$", "%.markdown$" } + }, + { + label = "Dictionary File", + description = "Path to a text file that contains a list of dictionary words.", + path = "dictionary_file", + type = "file", + exists = true, + default = platform_dictionary_file, + on_apply = function() + load_dictionary() + end + } +} + +load_dictionary() + command.add("core.docview", { + ["spell-check:toggle"] = function() + config.plugins.spellcheck.enabled = not config.plugins.spellcheck.enabled + end, + ["spell-check:add-to-dictionary"] = function() local word = get_word_at_caret() if words[word] then @@ -164,18 +215,28 @@ command.add("core.docview", { -- select word and init replacement selector local label = string.format("Replace \"%s\" With", word) doc:set_selection(line, e + 1, line, s) - core.command_view:enter(label, function(text, item) - text = item and item.text or text - doc:replace(function() return text end) - end, function(text) - local t = {} - for _, w in ipairs(suggestions) do - if w:lower():find(text:lower(), 1, true) then - table.insert(t, w) + core.command_view:enter(label, { + submit = function(text, item) + text = item and item.text or text + doc:replace(function() return text end) + end, + suggest = function(text) + local t = {} + for _, w in ipairs(suggestions) do + if w:lower():find(text:lower(), 1, true) then + table.insert(t, w) + end end + return t end - return t - end) + }) end, }) + +local contextmenu = require "plugins.contextmenu" +contextmenu:register("core.docview", { + contextmenu.DIVIDER, + { text = "View Suggestions", command = "spell-check:replace" }, + { text = "Add to Dictionary", command = "spell-check:add-to-dictionary" } +}) diff --git a/plugins/statusclock.lua b/plugins/statusclock.lua index 8289502..aec58e3 100644 --- a/plugins/statusclock.lua +++ b/plugins/statusclock.lua @@ -1,54 +1,88 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local style = require "core.style" +local common = require "core.common" local StatusView = require "core.statusview" -local scan_rate = 1 -config.plugins.statusclock = { +config.plugins.statusclock = common.merge({ + enabled = true, time_format = "%H:%M:%S", - date_format = "%A, %d %B %Y" -} + date_format = "%A, %d %B %Y", + -- The config specification used by the settings gui + config_spec = { + name = "Status Clock", + { + label = "Enabled", + description = "Show or hide the clock from the status bar.", + path = "enabled", + type = "toggle", + default = true, + on_apply = function(enabled) + core.add_thread(function() + if enabled then + core.status_view:get_item("status:clock"):show() + else + core.status_view:get_item("status:clock"):hide() + end + end) + end + }, + { + label = "Time Format", + description = "Time specification defined with Lua date/time place holders.", + path = "time_format", + type = "string", + default = "%H:%M:%S" + }, + { + label = "Date Format", + description = "Date specification defined with Lua date/time place holders.", + path = "date_format", + type = "string", + default = "%A, %d %B %Y", + } + } +}, config.plugins.statusclock) local time_data = { time_text = '', date_text = '', } -core.add_thread(function() - while true do +local last_time = os.time() +local function update_time() + if os.time() > last_time then local time_text = os.date(config.plugins.statusclock.time_format) local date_text = os.date(config.plugins.statusclock.date_format) - + if time_data.time_text ~= time_text or time_data.time_text ~= date_text then - core.redraw = true time_data.time_text = time_text time_data.date_text = date_text end - - coroutine.yield(scan_rate) - end -end) - -local get_items = StatusView.get_items - -function StatusView:get_items() - local left, right = get_items(self) - - local t = { - style.dim, - self.separator, - style.dim and style.text, - time_data.date_text, - style.dim, - self.separator, - style.dim and style.text, - time_data.time_text, - } - for _, item in ipairs(t) do - table.insert(right, item) + -- only redraw if seconds enabled + if config.plugins.statusclock.time_format:find("%S", 1, true) then + core.redraw = true + end + last_time = os.time() end - - return left, right end +core.status_view:add_item({ + name = "status:clock", + alignment = StatusView.Item.RIGHT, + get_item = function(self) + update_time() + return { + style.text, + time_data.date_text, + style.dim, + self.separator, + style.text, + time_data.time_text, + } + end, + position = -1, + separator = core.status_view.separator2 +}) + diff --git a/plugins/tabnumbers.lua b/plugins/tabnumbers.lua index 581a6b2..97e5d10 100644 --- a/plugins/tabnumbers.lua +++ b/plugins/tabnumbers.lua @@ -1,28 +1,44 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local config = require "core.config" local common = require "core.common" -local core = require "core" local style = require "core.style" +local Node = require "core.node" --- quite hackish, but Node isn't normally public -local Node = getmetatable(core.root_view.root_node) -local draw_tabs = Node.draw_tabs +config.plugins.tabnumbers = common.merge({ + enabled = true, + -- The config specification used by the settings gui + config_spec = { + name = "Tab Numbers", + { + label = "Draw Tab Numbers", + description = "Show or hide numbers on the interface tabs.", + path = "enabled", + type = "toggle", + default = true + } + } +}, config.plugins.tabnumbers) -function Node:draw_tabs(...) - draw_tabs(self, ...) - - for i, view in ipairs(self.views) do - if i > 9 then break end - - local x, y, w, h = self:get_tab_rect(i) - local number = tostring(i) - local color = style.dim - local title_width = style.font:get_width(view:get_name()) - local free_real_estate = - math.min(math.max((w - title_width) / 2, style.padding.x), h) - if view == self.active_view then - color = style.accent +-- Overwrite draw_tab_title to prepend tab number +local Node_draw_tab_title = Node.draw_tab_title +function Node:draw_tab_title(view, font, is_active, is_hovered, x, y, w, h) + if config.plugins.tabnumbers.enabled then + local number = "" + for i, v in ipairs(self.views) do + if view == v then + number = tostring(i) + end + end + local padx = 0 + if number ~= "" then + padx = style.font:get_width(number) + (style.padding.x / 2) + w = w - padx + local color = is_active and style.text or style.dim + common.draw_text(style.font, color, number, nil, x, y, w, h) end - -- renderer.draw_rect(x, y + h - 1, free_real_estate, 1, color) - common.draw_text(style.font, color, tostring(i), "center", x, y, free_real_estate, h) + local tx = x + padx -- Space for number + Node_draw_tab_title(self, view, font, is_active, is_hovered, tx, y, w, h) + else + Node_draw_tab_title(self, view, font, is_active, is_hovered, x, y, w, h) end end diff --git a/plugins/texcompile.lua b/plugins/texcompile.lua index 0c19d1b..af31aed 100644 --- a/plugins/texcompile.lua +++ b/plugins/texcompile.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local config = require "core.config" local command = require "core.command" diff --git a/plugins/themeselect.lua b/plugins/themeselect.lua index 70bd627..fe4ff34 100644 --- a/plugins/themeselect.lua +++ b/plugins/themeselect.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" -- Load a specific theme when the filename of an active document does match @@ -7,12 +7,11 @@ local core = require "core" -- usage: -- require("plugins.themeselect").add_pattern("%.md$", "summer") -local theme_select = { } +local theme_select = {} local saved_colors_module = "core.style" -local themes_patterns = { -} +local themes_patterns = {} local reload_module = core.reload_module local set_visited = core.set_visited diff --git a/plugins/titleize.lua b/plugins/titleize.lua index edb50d2..16d20b0 100644 --- a/plugins/titleize.lua +++ b/plugins/titleize.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" @@ -9,4 +9,3 @@ command.add("core.docview", { end) end, }) - diff --git a/plugins/togglesnakecamel.lua b/plugins/togglesnakecamel.lua index b5d20d9..c055933 100644 --- a/plugins/togglesnakecamel.lua +++ b/plugins/togglesnakecamel.lua @@ -1,4 +1,4 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/plugins/typingspeed.lua b/plugins/typingspeed.lua index 62e44ef..0e28ec9 100644 --- a/plugins/typingspeed.lua +++ b/plugins/typingspeed.lua @@ -1,4 +1,4 @@ --- mod-version:2 +-- mod-version:3 local core = require "core" local style = require "core.style" @@ -6,17 +6,38 @@ local common = require "core.common" local config = require "core.config" local DocView = require "core.docview" -if common["merge"] then - config.plugins.typingspeed = common.merge({ - -- characters that should be counted as word boundary - word_boundaries = "[%p%s]", - }, config.plugins.keystats) -else - config.plugins.typingspeed = { - -- characters that should be counted as word boundary - word_boundaries = "[%p%s]", - } -end +config.plugins.typingspeed = common.merge({ + enabled = true, + -- characters that should be counted as word boundary + word_boundaries = "[%p%s]", + -- The config specification used by the settings gui + config_spec = { + name = "Typing Speed", + { + label = "Enabled", + description = "Show or hide the typing speed from the status bar.", + path = "enabled", + type = "toggle", + default = true, + on_apply = function(enabled) + core.add_thread(function() + if enabled then + core.status_view:get_item("typing-speed:stats"):show() + else + core.status_view:get_item("typing-speed:stats"):hide() + end + end) + end + }, + { + label = "Word Boundaries", + description = "Lua pattern that matches characters to separate words.", + path = "word_boundaries", + type = "string", + default = "[%p%s]" + } + } +}, config.plugins.typingspeed) local chars = 0 local chars_last = 0 @@ -29,65 +50,51 @@ local wpm = 0 core.add_thread(function() while true do - local t = os.date("*t") - if t.sec <= time_last then - words_last = words - words = 0 - chars_last = chars - chars = 0 - time_last = t.sec - end - wpm = words_last * (1-(t.sec)/60) + words - cpm = chars_last * (1-(t.sec)/60) + chars + if config.plugins.typingspeed.enabled then + local t = os.date("*t") + if t.sec <= time_last then + words_last = words + words = 0 + chars_last = chars + chars = 0 + time_last = t.sec + end + wpm = words_last * (1-(t.sec)/60) + words + cpm = chars_last * (1-(t.sec)/60) + chars + end coroutine.yield(1) end end) local on_text_input = DocView.on_text_input function DocView:on_text_input(text, idx) - chars = chars + 1 - if string.find(text, config.plugins.typingspeed.word_boundaries) then - if started_word then - words = words + 1 - started_word = false - end - else - started_word = true - end + if config.plugins.typingspeed.enabled then + chars = chars + 1 + if string.find(text, config.plugins.typingspeed.word_boundaries) then + if started_word then + words = words + 1 + started_word = false + end + else + started_word = true + end + end on_text_input(self, text, idx) end -if core.status_view["add_item"] then - core.status_view:add_item( - function() - return core.active_view and getmetatable(core.active_view) == DocView - end, - "keystats:stats", - core.status_view.Item.RIGHT, - function() - return { - style.text, - string.format("%.0f CPM / %.0f WPM", cpm, wpm) - } - end, - nil, - 1, - "characters / words per minute" - ).separator = core.status_view.separator2 -else - local get_items = core.status_view.get_items - function core.status_view:get_items() - local left, right = get_items(self) - - local t = { - style.text, string.format("%.0f CPM / %.0f WPM", cpm, wpm), - style.dim, self.separator2 - } - - for i, item in ipairs(t) do - table.insert(right, i, item) - end - - return left, right - end -end +core.status_view:add_item({ + predicate = function() + return core.active_view and getmetatable(core.active_view) == DocView + end, + name = "typing-speed:stats", + alignment = core.status_view.Item.RIGHT, + get_item = function() + return { + style.text, + string.format("%.0f CPM / %.0f WPM", cpm, wpm) + } + end, + position = 1, + tooltip = "characters / words per minute", + separator = core.status_view.separator2 +}) diff --git a/plugins/unboundedscroll.lua b/plugins/unboundedscroll.lua index a27ab25..0793cfa 100644 --- a/plugins/unboundedscroll.lua +++ b/plugins/unboundedscroll.lua @@ -1,6 +1,18 @@ --- mod-version:2 -- lite-xl 2.0 +-- mod-version:3 +local command = require "core.command" local DocView = require "core.docview" -function DocView.clamp_scroll_position() - -- do nothing -end +local doc_view_clamp_scroll_position = DocView.clamp_scroll_position +local function clamp_scroll_noop() end + +DocView.clamp_scroll_position = clamp_scroll_noop + +command.add(nil, { + ["unbounded-scroll:toggle"] = function() + if DocView.clamp_scroll_position == clamp_scroll_noop then + DocView.clamp_scroll_position = doc_view_clamp_scroll_position + else + DocView.clamp_scroll_position = clamp_scroll_noop + end + end, +}) diff --git a/plugins/wordcount.lua b/plugins/wordcount.lua new file mode 100644 index 0000000..42224ac --- /dev/null +++ b/plugins/wordcount.lua @@ -0,0 +1,90 @@ +-- mod-version:3 +local core = require "core" +local style = require "core.style" +local StatusView = require "core.statusview" +local CommandView = require "core.commandview" +local DocView = require "core.docview" +local Doc = require "core.doc" +local keymap = require "core.keymap" + + +local words = setmetatable({}, { __mode = "k" }) + + +local function compute_line_words(line) + local s, total_words = 1, 0 + while true do + local ns, e = line:find("%s+", s) + if ns == 1 and e == #line then break end + if not e then total_words = math.max(total_words, 1) break end + total_words = total_words + 1 + s = e + 1 + end + return total_words +end + + +local function compute_words(doc, start_line, end_line) + local total_words = 0 + for i = start_line or 1, end_line or #doc.lines do + total_words = total_words + compute_line_words(doc.lines[i]) + end + return total_words +end + + +local old_raw_insert = Doc.raw_insert +function Doc:raw_insert(line, col, text, undo_stack, time) + if words[self] then + local old_count = compute_words(self, line, line) + old_raw_insert(self, line, col, text, undo_stack, time) + local total_lines, s = 0, 0 + while true do + s = text:find("\n", s + 1, true) + if not s then break end + total_lines = total_lines + 1 + end + words[self] = words[self] + compute_words(self, line, line + total_lines) - old_count + else + old_raw_insert(self, line, col, text, undo_stack, time) + end +end + + +local old_raw_remove = Doc.raw_remove +function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) + if words[self] then + local old_count = compute_words(self, line1, line2) + old_raw_remove(self, line1, col1, line2, col2, undo_stack, time) + words[self] = words[self] + compute_words(self, line1, line1) - old_count + else + old_raw_remove(self, line1, col1, line2, col2, undo_stack, time) + end +end + + +local old_doc_new = Doc.new +function Doc:new(...) + old_doc_new(self, ...) + words[self] = compute_words(self) +end + +local cached_word_length, cached_word_count + +core.status_view:add_item({ + predicate = function() return core.active_view:is(DocView) and not core.active_view:is(CommandView) and words[core.active_view.doc] end, + name = "status:word-count", + alignment = StatusView.Item.RIGHT, + get_item = function() + local selection_text = core.active_view.doc:get_selection_text() + if #selection_text ~= cached_word_length then + cached_word_count = compute_line_words(selection_text) + cached_word_length = #selection_text + end + if #selection_text > 0 then + return { style.text, cached_word_count .. " words" } + else + return { style.text, words[core.active_view.doc] .. " words" } + end + end +}) |