Last active
September 21, 2022 04:30
-
-
Save 0x715C/64f2ab39a56172d34541caa01db022b4 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
description = "Lightning-fast and Powerful Code Editor written in Rust "; | |
inputs = { | |
nixpkgs-unstable.url = github:nixos/nixpkgs/nixpkgs-unstable; | |
flake-utils.url = github:numtide/flake-utils; | |
}; | |
outputs = { self, nixpkgs-unstable, flake-utils }: | |
flake-utils.lib.eachSystem [ "x86_64-linux" ] (system: let | |
pkgs = import "${nixpkgs-unstable}" { | |
system = "${system}"; | |
}; | |
in | |
{ | |
packages.default = self.packages.${system}.lapce; | |
packages.lapce = pkgs.rustPlatform.buildRustPackage rec { | |
pname = "lapce"; | |
version = "0.2.0"; | |
src = pkgs.fetchFromGitHub { | |
owner = "lapce"; | |
repo = pname; | |
rev = "v${version}"; | |
sha256 = "sha256-cCcI5V6CMLkJM0miLv/o7LAJedrgb+z2CtWmF5/dmvY="; | |
}; | |
treesitterpatch = ./treesitterpatch.diff | |
cargoHash = "sha256-4njr6W42DnBItmz6Yrmu5pCR13/Bfv10BxqgoP4JKS0="; | |
cargoPatches = [ treesitterpatch ]; | |
nativeBuildInputs = with pkgs;[ | |
wrapGAppsHook | |
gobject-introspection | |
perl | |
cmake | |
pkg-config | |
copyDesktopItems | |
]; | |
# Get openssl-sys to use pkg-config | |
OPENSSL_NO_VENDOR = 1; | |
buildInputs = with pkgs;[ | |
glib | |
gtk3 | |
openssl | |
] ++ lib.optionals stdenv.isLinux [ | |
fontconfig | |
] ++ lib.optionals stdenv.isDarwin [ | |
libobjc | |
Security | |
CoreServices | |
ApplicationServices | |
Carbon | |
AppKit | |
]; | |
postInstall = '' | |
install -Dm0644 $src/extra/images/logo.svg $out/share/icons/hicolor/scalable/apps/lapce.svg | |
''; | |
desktopItems = [ (pkgs.makeDesktopItem { | |
name = "lapce"; | |
exec = "lapce %F"; | |
icon = "lapce"; | |
desktopName = "Lapce"; | |
comment = "Lightning-fast and Powerful Code Editor written in Rust"; | |
genericName = "Code Editor"; | |
categories = [ "Development" "Utility" "TextEditor" ]; | |
}) ]; | |
}; | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md | |
new file mode 100644 | |
index 000000000..2ea017b0c | |
--- /dev/null | |
+++ b/.github/ISSUE_TEMPLATE/bug_report.md | |
@@ -0,0 +1,29 @@ | |
+--- | |
+name: Bug report | |
+about: Create a report to help us improve | |
+title: '' | |
+labels: '' | |
+assignees: '' | |
+ | |
+--- | |
+ | |
+## Lapce Version | |
+ | |
+The Lapce version you are using, which can be found in "About Lapce" at the top right settings icon. | |
+ | |
+## System information | |
+ | |
+the operating system used, including its version, e.g. Windows 10, Ubuntu 18.04 | |
+ | |
+## Describe the bug | |
+A clear and concise description of what the bug is. | |
+ | |
+## Additional information | |
+ | |
+Other information that can be used to further reproduce or isolate the problem. | |
+This commonly includes: | |
+ | |
+- screenshots | |
+- logs | |
+- theories about what might be going wrong | |
+- workarounds that you used | |
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md | |
new file mode 100644 | |
index 000000000..148997aa0 | |
--- /dev/null | |
+++ b/.github/ISSUE_TEMPLATE/feature_request.md | |
@@ -0,0 +1,24 @@ | |
+--- | |
+name: Feature request | |
+about: Suggest an idea for this project | |
+title: '' | |
+labels: '' | |
+assignees: '' | |
+ | |
+--- | |
+ | |
+## Is your feature request related to a problem? Please describe. | |
+ | |
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] | |
+ | |
+## Describe the solution you'd like | |
+ | |
+A clear and concise description of what you want to happen. | |
+ | |
+## Describe alternatives you've considered | |
+ | |
+A clear and concise description of any alternative solutions or features you've considered. | |
+ | |
+## Additional context | |
+ | |
+Add any other context or screenshots about the feature request here. | |
diff --git a/Cargo.lock b/Cargo.lock | |
index 74fed462c..cb787ddef 100644 | |
--- a/Cargo.lock | |
+++ b/Cargo.lock | |
@@ -122,6 +122,12 @@ version = "1.0.61" | |
source = "registry+https://github.com/rust-lang/crates.io-index" | |
checksum = "508b352bb5c066aac251f6daf6b36eccd03e8a88e8081cd44959ea277a3af9a8" | |
+[[package]] | |
+name = "arc-swap" | |
+version = "1.5.1" | |
+source = "registry+https://github.com/rust-lang/crates.io-index" | |
+checksum = "983cd8b9d4b02a6dc6ffa557262eb5858a27a0038ffffe21a0f133eaa819a164" | |
+ | |
[[package]] | |
name = "arrayref" | |
version = "0.3.6" | |
@@ -2536,21 +2542,26 @@ dependencies = [ | |
name = "lapce-core" | |
version = "0.2.0" | |
dependencies = [ | |
+ "arc-swap", | |
"bitflags", | |
"itertools", | |
"lapce-rpc", | |
"log 0.4.17", | |
"lsp-types", | |
+ "once_cell", | |
"serde 1.0.143", | |
"serde_json", | |
+ "slotmap", | |
"strum 0.24.1", | |
"strum_macros 0.24.3", | |
"thiserror", | |
"tree-sitter", | |
"tree-sitter-bash", | |
"tree-sitter-c", | |
+ "tree-sitter-c-sharp", | |
"tree-sitter-cpp", | |
"tree-sitter-css", | |
+ "tree-sitter-dockerfile", | |
"tree-sitter-elixir", | |
"tree-sitter-elm", | |
"tree-sitter-glimmer", | |
@@ -2565,6 +2576,7 @@ dependencies = [ | |
"tree-sitter-json", | |
"tree-sitter-julia", | |
"tree-sitter-md", | |
+ "tree-sitter-nix", | |
"tree-sitter-ocaml", | |
"tree-sitter-php", | |
"tree-sitter-python", | |
@@ -3493,7 +3505,7 @@ dependencies = [ | |
[[package]] | |
name = "parley" | |
version = "0.1.0" | |
-source = "git+https://github.com/lapce/parley#6baaa02bbe3ab717822656ad98a95f8121febd53" | |
+source = "git+https://github.com/lapce/parley#c37477b889ff53b9a3033e2180becddf90b9bb17" | |
dependencies = [ | |
"fount", | |
"swash", | |
@@ -3575,7 +3587,7 @@ dependencies = [ | |
[[package]] | |
name = "piet-wgpu" | |
version = "0.1.0" | |
-source = "git+https://github.com/lapce/piet-wgpu?branch=shell_opengl#f863b3e49175d1811e6008cea2ca1bd282fc5bed" | |
+source = "git+https://github.com/lapce/piet-wgpu?branch=shell_opengl#516c591c0b78bf6446fc6b04c4f2e8a89c4ee0b7" | |
dependencies = [ | |
"bytemuck", | |
"glam", | |
@@ -3736,7 +3748,7 @@ dependencies = [ | |
[[package]] | |
name = "psp-types" | |
version = "0.1.0" | |
-source = "git+https://github.com/lapce/psp-types#0a18e1d285b6e1791e440728c633ef84ed99d1e0" | |
+source = "git+https://github.com/lapce/psp-types#e7305b0bda539208dd133732178c3e197f95ca5f" | |
dependencies = [ | |
"lsp-types", | |
"serde 1.0.143", | |
@@ -4945,6 +4957,15 @@ dependencies = [ | |
"tree-sitter", | |
] | |
+[[package]] | |
+name = "tree-sitter-c-sharp" | |
+version = "0.19.1" | |
+source = "git+https://github.com/tree-sitter/tree-sitter-c-sharp?branch=master#2734945fcc2da52365e7071958b9eba4ff151937" | |
+dependencies = [ | |
+ "cc", | |
+ "tree-sitter", | |
+] | |
+ | |
[[package]] | |
name = "tree-sitter-cpp" | |
version = "0.20.0" | |
@@ -4964,6 +4985,15 @@ dependencies = [ | |
"tree-sitter", | |
] | |
+[[package]] | |
+name = "tree-sitter-dockerfile" | |
+version = "0.1.0" | |
+source = "git+https://github.com/panekj/tree-sitter-dockerfile?branch=queries#c49d819e07685c90456270f1cc654d9cba640f53" | |
+dependencies = [ | |
+ "cc", | |
+ "tree-sitter", | |
+] | |
+ | |
[[package]] | |
name = "tree-sitter-elixir" | |
version = "0.19.0" | |
@@ -5089,8 +5119,17 @@ dependencies = [ | |
[[package]] | |
name = "tree-sitter-md" | |
+version = "0.1.1" | |
+source = "git+https://github.com/MDeiml/tree-sitter-markdown.git?branch=split_parser#e375ba95ff9a12418f9b9e7c190f549d08b5380a" | |
+dependencies = [ | |
+ "cc", | |
+ "tree-sitter", | |
+] | |
+ | |
+[[package]] | |
+name = "tree-sitter-nix" | |
version = "0.0.1" | |
-source = "git+https://github.com/dzhou121/tree-sitter-markdown.git#134c7f870ad17b3c3e40cd90b8bbd08329ac4bbd" | |
+source = "git+https://github.com/panekj/tree-sitter-nix?branch=master#59fc47150ab437e8bb356c7ab21e9531e87f7cc8" | |
dependencies = [ | |
"cc", | |
"tree-sitter", | |
diff --git a/README.md b/README.md | |
index 47098adb2..20023ef74 100644 | |
--- a/README.md | |
+++ b/README.md | |
@@ -129,4 +129,4 @@ There is also a [Matrix Space](https://matrix.to/#/#lapce-editor:matrix.org), wh | |
## License | |
-Lapce is released under the Apache License Version 2, which is an open source license. You may contribute to this project, or use the code as you please as long as you adhere to its conditions. You can find a copy of the license text within `LICENSE`. | |
+Lapce is released under the Apache License Version 2, which is an open source license. You may contribute to this project, or use the code as you please as long as you adhere to its conditions. You can find a copy of the license text here: [`LICENSE`](LICENSE). | |
diff --git a/defaults/dark-theme.toml b/defaults/dark-theme.toml | |
index 6cbd7803e..9eccea4ea 100644 | |
--- a/defaults/dark-theme.toml | |
+++ b/defaults/dark-theme.toml | |
@@ -50,7 +50,13 @@ magenta = "#C678DD" | |
"type.builtin" = "$cyan" | |
"builtinType" = "$cyan" | |
"escape" = "$cyan" | |
+"string.escape" = "$cyan" | |
"embedded" = "$cyan" | |
+"punctuation.delimiter" = "$yellow" | |
+"text.title" = "$orange" | |
+"text.uri" = "$cyan" | |
+"text.reference" = "$yellow" | |
+ | |
[theme.ui] | |
"lapce.active_tab" = "$black" | |
diff --git a/defaults/light-theme.toml b/defaults/light-theme.toml | |
index 21774a9f2..8533826d9 100644 | |
--- a/defaults/light-theme.toml | |
+++ b/defaults/light-theme.toml | |
@@ -52,12 +52,18 @@ magenta = "#A626A4" | |
"variable.other.member" = "$red" | |
"string" = "$green" | |
+"string.escape" = "$cyan" | |
"type.builtin" = "$cyan" | |
"builtinType" = "$cyan" | |
"escape" = "$cyan" | |
"embedded" = "$cyan" | |
+"punctuation.delimiter" = "$yellow" | |
+"text.title" = "$orange" | |
+"text.uri" = "$cyan" | |
+"text.reference" = "$yellow" | |
+ | |
[theme.ui] | |
"lapce.active_tab" = "$white" | |
"lapce.inactive_tab" = "$grey" | |
diff --git a/lapce-core/Cargo.toml b/lapce-core/Cargo.toml | |
index 68e4e960d..0acbd8178 100644 | |
--- a/lapce-core/Cargo.toml | |
+++ b/lapce-core/Cargo.toml | |
@@ -9,6 +9,9 @@ thiserror = "1.0" | |
itertools = "0.10.3" | |
log = "0.4.14" | |
bitflags = "1.3.2" | |
+once_cell = "1.13.0" | |
+slotmap = "1.0" | |
+arc-swap = "1.5.1" | |
strum = "0.24.0" | |
strum_macros = "0.24" | |
serde = "1.0" | |
@@ -30,7 +33,7 @@ tree-sitter-ruby = { git = "https://github.com/Liberatys/tree-sitter-ruby.git", | |
tree-sitter-c = { version = "0.20.1", optional = true } | |
tree-sitter-cpp = { version = "0.20.0", optional = true } | |
tree-sitter-json = { version = "0.19.0", optional = true } | |
-tree-sitter-md = { git = "https://github.com/dzhou121/tree-sitter-markdown.git", version = "0.0.1", optional = true } | |
+tree-sitter-md = { git = "https://github.com/MDeiml/tree-sitter-markdown.git", branch = "split_parser", optional = true } | |
tree-sitter-html = { version = "0.19.0", optional = true } | |
tree-sitter-java = { git = "https://github.com/tree-sitter/tree-sitter-java.git", version = "0.20.0", optional = true } | |
tree-sitter-elm = { version = "5.6.0", optional = true } | |
@@ -51,6 +54,9 @@ tree-sitter-bash = { git = "https://github.com/syntacti/tree-sitter-bash", branc | |
tree-sitter-yaml = { git = "https://github.com/panekj/tree-sitter-yaml", branch = "master", optional = true } | |
tree-sitter-julia = { git = "https://github.com/varlad/tree-sitter-julia", branch = "master", optional = true } | |
tree-sitter-wgsl = { git = "https://github.com/szebniok/tree-sitter-wgsl", branch = "master", optional = true } | |
+tree-sitter-dockerfile = { git = "https://github.com/panekj/tree-sitter-dockerfile", branch = "queries", optional = true } | |
+tree-sitter-c-sharp = { git = "https://github.com/tree-sitter/tree-sitter-c-sharp", branch = "master", optional = true } | |
+tree-sitter-nix = { git = "https://github.com/panekj/tree-sitter-nix", branch = "master", optional = true } | |
[features] | |
default = [] | |
@@ -87,4 +93,7 @@ lang-zig = ["dep:tree-sitter-zig"] | |
lang-bash = ["dep:tree-sitter-bash"] | |
lang-yaml = ["dep:tree-sitter-yaml"] | |
lang-julia = ["dep:tree-sitter-julia"] | |
-lang-wgsl = ["dep:tree-sitter-wgsl"] | |
\ No newline at end of file | |
+lang-wgsl = ["dep:tree-sitter-wgsl"] | |
+lang-dockerfile = ["dep:tree-sitter-dockerfile"] | |
+lang-csharp = ["dep:tree-sitter-c-sharp"] | |
+lang-nix = ["dep:tree-sitter-nix"] | |
diff --git a/lapce-core/queries/markdown.inline/highlights.scm b/lapce-core/queries/markdown.inline/highlights.scm | |
new file mode 100644 | |
index 000000000..df8fb3d0c | |
--- /dev/null | |
+++ b/lapce-core/queries/markdown.inline/highlights.scm | |
@@ -0,0 +1,91 @@ | |
+;; From MDeiml/tree-sitter-markdown | |
+[ | |
+ (code_span) | |
+ (link_title) | |
+] @text.literal | |
+ | |
+[ | |
+ (emphasis_delimiter) | |
+ (code_span_delimiter) | |
+] @punctuation.delimiter | |
+ | |
+(emphasis) @text.emphasis | |
+ | |
+(strong_emphasis) @text.strong | |
+ | |
+[ | |
+ (link_destination) | |
+ (uri_autolink) | |
+] @text.uri | |
+ | |
+[ | |
+ (link_label) | |
+ (link_text) | |
+ (image_description) | |
+] @text.reference | |
+ | |
+[ | |
+ (backslash_escape) | |
+ (hard_line_break) | |
+] @string.escape | |
+ | |
+; "(" not part of query because of | |
+; https://github.com/nvim-treesitter/nvim-treesitter/issues/2206 | |
+; TODO: Find better fix for this | |
+(image ["!" "[" "]" "("] @punctuation.delimiter) | |
+(inline_link ["[" "]" "("] @punctuation.delimiter) | |
+(shortcut_link ["[" "]"] @punctuation.delimiter) | |
+ | |
+([ | |
+ (code_span_delimiter) | |
+ (emphasis_delimiter) | |
+] @conceal | |
+(#set! conceal "")) | |
+ | |
+; Conceal inline links | |
+(inline_link | |
+ [ | |
+ "[" | |
+ "]" | |
+ "(" | |
+ (link_destination) | |
+ ")" | |
+ ] @conceal | |
+ (#set! conceal "")) | |
+ | |
+; Conceal image links | |
+(image | |
+ [ | |
+ "!" | |
+ "[" | |
+ "]" | |
+ "(" | |
+ (link_destination) | |
+ ")" | |
+ ] @conceal | |
+ (#set! conceal "")) | |
+ | |
+; Conceal full reference links | |
+(full_reference_link | |
+ [ | |
+ "[" | |
+ "]" | |
+ (link_label) | |
+ ] @conceal | |
+ (#set! conceal "")) | |
+ | |
+; Conceal collapsed reference links | |
+(collapsed_reference_link | |
+ [ | |
+ "[" | |
+ "]" | |
+ ] @conceal | |
+ (#set! conceal "")) | |
+ | |
+; Conceal shortcut links | |
+(shortcut_link | |
+ [ | |
+ "[" | |
+ "]" | |
+ ] @conceal | |
+ (#set! conceal "")) | |
\ No newline at end of file | |
diff --git a/lapce-core/queries/markdown.inline/injections.scm b/lapce-core/queries/markdown.inline/injections.scm | |
new file mode 100644 | |
index 000000000..c137a8222 | |
--- /dev/null | |
+++ b/lapce-core/queries/markdown.inline/injections.scm | |
@@ -0,0 +1,2 @@ | |
+ | |
+((html_tag) @injection.content (#set! injection.language "html")) | |
diff --git a/lapce-core/queries/markdown/highlights.scm b/lapce-core/queries/markdown/highlights.scm | |
new file mode 100644 | |
index 000000000..ecd0596a8 | |
--- /dev/null | |
+++ b/lapce-core/queries/markdown/highlights.scm | |
@@ -0,0 +1,58 @@ | |
+;From MDeiml/tree-sitter-markdown | |
+(atx_heading (inline) @text.title) | |
+(setext_heading (paragraph) @text.title) | |
+ | |
+[ | |
+ (atx_h1_marker) | |
+ (atx_h2_marker) | |
+ (atx_h3_marker) | |
+ (atx_h4_marker) | |
+ (atx_h5_marker) | |
+ (atx_h6_marker) | |
+ (setext_h1_underline) | |
+ (setext_h2_underline) | |
+] @punctuation.special | |
+ | |
+[ | |
+ (link_title) | |
+ (indented_code_block) | |
+ (fenced_code_block) | |
+] @text.literal | |
+ | |
+[ | |
+ (fenced_code_block_delimiter) | |
+] @punctuation.delimiter | |
+ | |
+(code_fence_content) @none | |
+ | |
+[ | |
+ (link_destination) | |
+] @text.uri | |
+ | |
+[ | |
+ (link_label) | |
+] @text.reference | |
+ | |
+[ | |
+ (list_marker_plus) | |
+ (list_marker_minus) | |
+ (list_marker_star) | |
+ (list_marker_dot) | |
+ (list_marker_parenthesis) | |
+ (thematic_break) | |
+] @punctuation.special | |
+ | |
+[ | |
+ (block_continuation) | |
+ (block_quote_marker) | |
+] @punctuation.special | |
+ | |
+[ | |
+ (backslash_escape) | |
+] @string.escape | |
+ | |
+([ | |
+ (info_string) | |
+ (fenced_code_block_delimiter) | |
+] @conceal | |
+(#set! conceal "")) | |
\ No newline at end of file | |
diff --git a/lapce-core/queries/markdown/injections.scm b/lapce-core/queries/markdown/injections.scm | |
new file mode 100644 | |
index 000000000..16f7936bf | |
--- /dev/null | |
+++ b/lapce-core/queries/markdown/injections.scm | |
@@ -0,0 +1,15 @@ | |
+; From nvim-treesitter/nvim-treesitter | |
+ | |
+(fenced_code_block | |
+ (info_string | |
+ (language) @injection.language) | |
+ (code_fence_content) @injection.content (#set! injection.include-unnamed-children)) | |
+ | |
+((html_block) @injection.content (#set! injection.language "html") (#set! injection.include-unnamed-children)) | |
+ | |
+([ | |
+ (minus_metadata) | |
+ (plus_metadata) | |
+] @injection.content (#set! injection.language "yaml") (#set! injection.include-unnamed-children)) | |
+ | |
+((inline) @injection.content (#set! injection.language "markdown.inline") (#set! injection.include-unnamed-children)) | |
\ No newline at end of file | |
diff --git a/lapce-core/src/buffer.rs b/lapce-core/src/buffer.rs | |
index c121301c7..3d125dfbc 100644 | |
--- a/lapce-core/src/buffer.rs | |
+++ b/lapce-core/src/buffer.rs | |
@@ -14,7 +14,8 @@ use xi_rope::{ | |
diff::{Diff, LineHashDiff}, | |
interval::IntervalBounds, | |
multiset::Subset, | |
- Cursor, Delta, DeltaBuilder, Interval, Rope, RopeDelta, | |
+ tree::{Node, NodeInfo}, | |
+ Cursor, Delta, DeltaBuilder, DeltaElement, Interval, Rope, RopeDelta, | |
}; | |
use crate::{ | |
@@ -1431,5 +1432,54 @@ impl<I: Iterator<Item = (usize, char)>, O: Iterator<Item = I>> Iterator | |
} | |
} | |
+pub struct DeltaValueRegion<'a, N: NodeInfo + 'a> { | |
+ pub old_offset: usize, | |
+ pub new_offset: usize, | |
+ pub len: usize, | |
+ pub node: &'a Node<N>, | |
+} | |
+ | |
+/// Modified version of `xi_rope::delta::InsertsIter` which includes the node | |
+pub struct InsertsValueIter<'a, N: NodeInfo + 'a> { | |
+ pos: usize, | |
+ last_end: usize, | |
+ els_iter: std::slice::Iter<'a, DeltaElement<N>>, | |
+} | |
+impl<'a, N: NodeInfo + 'a> InsertsValueIter<'a, N> { | |
+ pub fn new(delta: &'a Delta<N>) -> InsertsValueIter<'a, N> { | |
+ InsertsValueIter { | |
+ pos: 0, | |
+ last_end: 0, | |
+ els_iter: delta.els.iter(), | |
+ } | |
+ } | |
+} | |
+impl<'a, N: NodeInfo> Iterator for InsertsValueIter<'a, N> { | |
+ type Item = DeltaValueRegion<'a, N>; | |
+ | |
+ fn next(&mut self) -> Option<Self::Item> { | |
+ for elem in &mut self.els_iter { | |
+ match *elem { | |
+ DeltaElement::Copy(b, e) => { | |
+ self.pos += e - b; | |
+ self.last_end = e; | |
+ } | |
+ DeltaElement::Insert(ref n) => { | |
+ let result = Some(DeltaValueRegion { | |
+ old_offset: self.last_end, | |
+ new_offset: self.pos, | |
+ len: n.len(), | |
+ node: n, | |
+ }); | |
+ self.pos += n.len(); | |
+ self.last_end += n.len(); | |
+ return result; | |
+ } | |
+ } | |
+ } | |
+ None | |
+ } | |
+} | |
+ | |
#[cfg(test)] | |
mod test; | |
diff --git a/lapce-core/src/editor.rs b/lapce-core/src/editor.rs | |
index 785ea9e73..e40fa193d 100644 | |
--- a/lapce-core/src/editor.rs | |
+++ b/lapce-core/src/editor.rs | |
@@ -11,8 +11,11 @@ use crate::{ | |
register::{Clipboard, Register, RegisterData, RegisterKind}, | |
selection::{InsertDrift, SelRegion, Selection}, | |
syntax::{ | |
- has_unmatched_pair, matching_char, matching_pair_direction, | |
- str_is_pair_left, str_matching_pair, Syntax, | |
+ util::{ | |
+ has_unmatched_pair, matching_char, matching_pair_direction, | |
+ str_is_pair_left, str_matching_pair, | |
+ }, | |
+ Syntax, | |
}, | |
word::{get_word_property, WordProperty}, | |
}; | |
diff --git a/lapce-core/src/language.rs b/lapce-core/src/language.rs | |
index 088344e68..84beb4f7c 100644 | |
--- a/lapce-core/src/language.rs | |
+++ b/lapce-core/src/language.rs | |
@@ -1,9 +1,9 @@ | |
use std::{collections::HashSet, path::Path, str::FromStr}; | |
use strum_macros::{Display, EnumString}; | |
-use tree_sitter::{Parser, TreeCursor}; | |
+use tree_sitter::TreeCursor; | |
-use crate::style::HighlightConfiguration; | |
+use crate::syntax::highlight::HighlightConfiguration; | |
// | |
// To add support for an hypothetical language called Foo, for example, using | |
@@ -42,6 +42,7 @@ use crate::style::HighlightConfiguration; | |
// id: LapceLanguage::Foo, | |
// language: tree_sitter_foo::language, | |
// highlight: tree_sitter_foo::HIGHLIGHT_QUERY, | |
+// injection: Some(tree_sitter_foo::INJECTION_QUERY), // or None if there is no injections | |
// comment: "//", | |
// indent: " ", | |
// code_lens: (&[/* ... */], &[/* ... */]), | |
@@ -49,7 +50,10 @@ use crate::style::HighlightConfiguration; | |
// }, | |
// ]; | |
// | |
-// 5. Add a new feature, say "lang-foo", to the lapce-ui crate (see | |
+// 5. In `syntax.rs`, add `Foo: "lang-foo",` to the list in the | |
+// `declare_language_highlights` macro. | |
+// | |
+// 6. Add a new feature, say "lang-foo", to the lapce-ui crate (see | |
// lapce-ui/Cargo.toml). | |
// | |
// [features] | |
@@ -75,6 +79,9 @@ struct SyntaxProperties { | |
language: fn() -> tree_sitter::Language, | |
/// For most languages, it is `tree_sitter_$crate::HIGHLIGHT_QUERY`. | |
highlight: &'static str, | |
+ /// For most languages, it is `tree_sitter_$crate::INJECTION_QUERY`. | |
+ /// Though, not all languages have injections. | |
+ injection: Option<&'static str>, | |
/// The comment token. "#" for python, "//" for rust for example. | |
comment: &'static str, | |
/// The indent unit. "\t" for python, " " for rust, for example. | |
@@ -126,6 +133,10 @@ pub enum LapceLanguage { | |
Json, | |
#[cfg(feature = "lang-markdown")] | |
Markdown, | |
+ // TODO: Hide this when it is shown to the user! | |
+ #[cfg(feature = "lang-markdown")] | |
+ #[strum(serialize = "markdown.inline")] | |
+ MarkdownInline, | |
#[cfg(feature = "lang-ruby")] | |
Ruby, | |
#[cfg(feature = "lang-html")] | |
@@ -137,7 +148,7 @@ pub enum LapceLanguage { | |
#[cfg(feature = "lang-swift")] | |
Swift, | |
#[cfg(feature = "lang-ql")] | |
- QL, | |
+ Ql, | |
#[cfg(feature = "lang-haskell")] | |
Haskell, | |
#[cfg(feature = "lang-glimmer")] | |
@@ -145,13 +156,13 @@ pub enum LapceLanguage { | |
#[cfg(feature = "lang-haxe")] | |
Haxe, | |
#[cfg(feature = "lang-hcl")] | |
- HCL, | |
+ Hcl, | |
#[cfg(feature = "lang-ocaml")] | |
- OCaml, | |
+ Ocaml, | |
#[cfg(feature = "lang-ocaml")] | |
- OCamlInterface, | |
+ OcamlInterface, | |
#[cfg(feature = "lang-scss")] | |
- SCSS, | |
+ Scss, | |
#[cfg(feature = "lang-hare")] | |
Hare, | |
#[cfg(feature = "lang-css")] | |
@@ -159,6 +170,7 @@ pub enum LapceLanguage { | |
#[cfg(feature = "lang-zig")] | |
Zig, | |
#[cfg(feature = "lang-bash")] | |
+ #[strum(serialize = "bash", serialize = "sh")] | |
Bash, | |
#[cfg(feature = "lang-yaml")] | |
Yaml, | |
@@ -166,6 +178,12 @@ pub enum LapceLanguage { | |
Julia, | |
#[cfg(feature = "lang-wgsl")] | |
Wgsl, | |
+ #[cfg(feature = "lang-dockerfile")] | |
+ Dockerfile, | |
+ #[cfg(feature = "lang-csharp")] | |
+ Csharp, | |
+ #[cfg(feature = "lang-nix")] | |
+ Nix, | |
} | |
// NOTE: Elements in the array must be in the same order as the enum variants of | |
@@ -176,6 +194,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Rust, | |
language: tree_sitter_rust::language, | |
highlight: tree_sitter_rust::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: ( | |
@@ -189,6 +208,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Go, | |
language: tree_sitter_go::language, | |
highlight: tree_sitter_go::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: ( | |
@@ -208,16 +228,19 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Javascript, | |
language: tree_sitter_javascript::language, | |
highlight: tree_sitter_javascript::HIGHLIGHT_QUERY, | |
+ injection: Some(tree_sitter_javascript::INJECTION_QUERY), | |
comment: "//", | |
indent: " ", | |
code_lens: (&["source_file", "program"], &["source_file"]), | |
- extensions: &["js"], | |
+ extensions: &["js", "cjs", "mjs"], | |
}, | |
#[cfg(feature = "lang-javascript")] | |
SyntaxProperties { | |
id: LapceLanguage::Jsx, | |
language: tree_sitter_javascript::language, | |
highlight: tree_sitter_javascript::JSX_HIGHLIGHT_QUERY, | |
+ // TODO: Does jsx use the javascript injection query too? | |
+ injection: Some(tree_sitter_javascript::INJECTION_QUERY), | |
comment: "//", | |
indent: " ", | |
code_lens: (&["source_file", "program"], &["source_file"]), | |
@@ -228,16 +251,18 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Typescript, | |
language: tree_sitter_typescript::language_typescript, | |
highlight: tree_sitter_typescript::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (&["source_file", "program"], &["source_file"]), | |
- extensions: &["ts"], | |
+ extensions: &["ts", "cts", "mts"], | |
}, | |
#[cfg(feature = "lang-typescript")] | |
SyntaxProperties { | |
id: LapceLanguage::Tsx, | |
language: tree_sitter_typescript::language_tsx, | |
highlight: tree_sitter_typescript::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (&["source_file", "program"], &["source_file"]), | |
@@ -248,6 +273,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Python, | |
language: tree_sitter_python::language, | |
highlight: tree_sitter_python::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "#", | |
indent: "\t", | |
code_lens: ( | |
@@ -269,6 +295,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Toml, | |
language: tree_sitter_toml::language, | |
highlight: tree_sitter_toml::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "#", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -279,6 +306,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Php, | |
language: tree_sitter_php::language, | |
highlight: tree_sitter_php::HIGHLIGHT_QUERY, | |
+ injection: Some(tree_sitter_php::INJECTIONS_QUERY), | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -289,6 +317,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Elixir, | |
language: tree_sitter_elixir::language, | |
highlight: tree_sitter_elixir::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "#", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -299,6 +328,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::C, | |
language: tree_sitter_c::language, | |
highlight: tree_sitter_c::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -309,6 +339,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Cpp, | |
language: tree_sitter_cpp::language, | |
highlight: tree_sitter_cpp::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -319,6 +350,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Json, | |
language: tree_sitter_json::language, | |
highlight: tree_sitter_json::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -328,17 +360,31 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
SyntaxProperties { | |
id: LapceLanguage::Markdown, | |
language: tree_sitter_md::language, | |
- highlight: tree_sitter_md::HIGHLIGHT_QUERY, | |
+ highlight: include_str!("../queries/markdown/highlights.scm"), | |
+ injection: Some(include_str!("../queries/markdown/injections.scm")), | |
comment: "", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
extensions: &["md"], | |
}, | |
+ #[cfg(feature = "lang-markdown")] | |
+ SyntaxProperties { | |
+ id: LapceLanguage::MarkdownInline, | |
+ language: tree_sitter_md::inline_language, | |
+ highlight: include_str!("../queries/markdown.inline/highlights.scm"), | |
+ injection: Some(include_str!("../queries/markdown.inline/injections.scm")), | |
+ comment: "", | |
+ indent: " ", | |
+ code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
+ // markdown inline is only used as an injection by the Markdown language | |
+ extensions: &[], | |
+ }, | |
#[cfg(feature = "lang-ruby")] | |
SyntaxProperties { | |
id: LapceLanguage::Ruby, | |
language: tree_sitter_ruby::language, | |
highlight: tree_sitter_ruby::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "#", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -349,6 +395,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Html, | |
language: tree_sitter_html::language, | |
highlight: tree_sitter_html::HIGHLIGHT_QUERY, | |
+ injection: Some(tree_sitter_html::INJECTION_QUERY), | |
comment: "", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -359,6 +406,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Java, | |
language: tree_sitter_java::language, | |
highlight: tree_sitter_java::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -369,6 +417,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Elm, | |
language: tree_sitter_elm::language, | |
highlight: tree_sitter_elm::HIGHLIGHTS_QUERY, | |
+ injection: Some(tree_sitter_elm::INJECTIONS_QUERY), | |
comment: "#", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -379,6 +428,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Swift, | |
language: tree_sitter_swift::language, | |
highlight: tree_sitter_swift::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -386,9 +436,10 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
}, | |
#[cfg(feature = "lang-ql")] | |
SyntaxProperties { | |
- id: LapceLanguage::QL, | |
+ id: LapceLanguage::Ql, | |
language: tree_sitter_ql::language, | |
highlight: tree_sitter_ql::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -399,6 +450,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Haskell, | |
language: tree_sitter_haskell::language, | |
highlight: tree_sitter_haskell::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "--", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -409,6 +461,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Glimmer, | |
language: tree_sitter_glimmer::language, | |
highlight: tree_sitter_glimmer::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "{{!", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -419,6 +472,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Haxe, | |
language: tree_sitter_haxe::language, | |
highlight: tree_sitter_haxe::HIGHLIGHTS_QUERY, | |
+ injection: Some(tree_sitter_haxe::INJECTIONS_QUERY), | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -426,9 +480,10 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
}, | |
#[cfg(feature = "lang-hcl")] | |
SyntaxProperties { | |
- id: LapceLanguage::HCL, | |
+ id: LapceLanguage::Hcl, | |
language: tree_sitter_hcl::language, | |
highlight: tree_sitter_hcl::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -436,9 +491,10 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
}, | |
#[cfg(feature = "lang-ocaml")] | |
SyntaxProperties { | |
- id: LapceLanguage::OCaml, | |
+ id: LapceLanguage::Ocaml, | |
language: tree_sitter_ocaml::language_ocaml, | |
highlight: tree_sitter_ocaml::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "(*", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -446,9 +502,10 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
}, | |
#[cfg(feature = "lang-ocaml")] | |
SyntaxProperties { | |
- id: LapceLanguage::OCaml, | |
+ id: LapceLanguage::Ocaml, | |
language: tree_sitter_ocaml::language_ocaml_interface, | |
highlight: tree_sitter_ocaml::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "(*", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -456,9 +513,10 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
}, | |
#[cfg(feature = "lang-scss")] | |
SyntaxProperties { | |
- id: LapceLanguage::SCSS, | |
+ id: LapceLanguage::Scss, | |
language: tree_sitter_scss::language, | |
highlight: tree_sitter_scss::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -469,6 +527,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Hare, | |
language: tree_sitter_hare::language, | |
highlight: tree_sitter_hare::HIGHLIGHT_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -479,6 +538,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Css, | |
language: tree_sitter_css::language, | |
highlight: tree_sitter_css::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "/*", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -489,6 +549,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Zig, | |
language: tree_sitter_zig::language, | |
highlight: tree_sitter_zig::HIGHLIGHTS_QUERY, | |
+ injection: Some(tree_sitter_zig::INJECTIONS_QUERY), | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -499,6 +560,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Bash, | |
language: tree_sitter_bash::language, | |
highlight: tree_sitter_bash::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "#", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -509,6 +571,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Yaml, | |
language: tree_sitter_yaml::language, | |
highlight: tree_sitter_yaml::HIGHLIGHTS_QUERY, | |
+ injection: Some(tree_sitter_yaml::INJECTIONS_QUERY), | |
comment: "#", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -519,6 +582,7 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Julia, | |
language: tree_sitter_julia::language, | |
highlight: include_str!("../queries/julia/highlights.scm"), | |
+ injection: None, | |
comment: "#", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
@@ -529,11 +593,45 @@ const LANGUAGES: &[SyntaxProperties] = &[ | |
id: LapceLanguage::Wgsl, | |
language: tree_sitter_wgsl::language, | |
highlight: tree_sitter_wgsl::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
comment: "//", | |
indent: " ", | |
code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
extensions: &["wgsl"], | |
}, | |
+ #[cfg(feature = "lang-dockerfile")] | |
+ SyntaxProperties { | |
+ id: LapceLanguage::Dockerfile, | |
+ language: tree_sitter_dockerfile::language, | |
+ highlight: tree_sitter_dockerfile::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
+ comment: "#", | |
+ indent: " ", | |
+ code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
+ extensions: &["dockerfile"], | |
+ }, | |
+ #[cfg(feature = "lang-csharp")] | |
+ SyntaxProperties { | |
+ id: LapceLanguage::Csharp, | |
+ language: tree_sitter_c_sharp::language, | |
+ highlight: tree_sitter_c_sharp::HIGHLIGHT_QUERY, | |
+ injection: None, | |
+ comment: "#", | |
+ indent: " ", | |
+ code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
+ extensions: &["cs", "csx"], | |
+ }, | |
+ #[cfg(feature = "lang-nix")] | |
+ SyntaxProperties { | |
+ id: LapceLanguage::Nix, | |
+ language: tree_sitter_nix::language, | |
+ highlight: tree_sitter_nix::HIGHLIGHTS_QUERY, | |
+ injection: None, | |
+ comment: "#", | |
+ indent: " ", | |
+ code_lens: (DEFAULT_CODE_LENS_LIST, DEFAULT_CODE_LENS_IGNORE_LIST), | |
+ extensions: &["nix"], | |
+ }, | |
]; | |
impl LapceLanguage { | |
@@ -590,18 +688,14 @@ impl LapceLanguage { | |
self.properties().indent | |
} | |
- pub(crate) fn new_parser(&self) -> Parser { | |
- let language = (self.properties().language)(); | |
- let mut parser = Parser::new(); | |
- parser.set_language(language).unwrap(); | |
- parser | |
- } | |
- | |
pub(crate) fn new_highlight_config(&self) -> HighlightConfiguration { | |
- let language = (self.properties().language)(); | |
- let query = self.properties().highlight; | |
+ let props = self.properties(); | |
+ let language = (props.language)(); | |
+ let query = props.highlight; | |
+ let injection = props.injection; | |
- HighlightConfiguration::new(language, query, "", "").unwrap() | |
+ HighlightConfiguration::new(language, query, injection.unwrap_or(""), "") | |
+ .unwrap() | |
} | |
pub(crate) fn walk_tree( | |
@@ -783,7 +877,7 @@ mod test { | |
#[test] | |
#[cfg(feature = "lang-ql")] | |
fn test_ql_lang() { | |
- assert_language(LapceLanguage::QL, &["ql"]); | |
+ assert_language(LapceLanguage::Ql, &["ql"]); | |
} | |
#[test] | |
#[cfg(feature = "lang-haskell")] | |
@@ -800,16 +894,16 @@ mod test { | |
} | |
#[cfg(feature = "lang-hcl")] | |
fn test_hcl_lang() { | |
- assert_language(LapceLanguage::HCL, &["hcl"]); | |
+ assert_language(LapceLanguage::Hcl, &["hcl"]); | |
} | |
#[cfg(feature = "lang-ocaml")] | |
fn test_ocaml_lang() { | |
- assert_language(LapceLanguage::OCaml, &["ml"]); | |
- assert_language(LapceLanguage::OCamlInterface, &["mli"]); | |
+ assert_language(LapceLanguage::Ocaml, &["ml"]); | |
+ assert_language(LapceLanguage::OcamlInterface, &["mli"]); | |
} | |
#[cfg(feature = "lang-scss")] | |
fn test_scss_lang() { | |
- assert_language(LapceLanguage::SCSS, &["scss"]); | |
+ assert_language(LapceLanguage::Scss, &["scss"]); | |
} | |
#[cfg(feature = "lang-hare")] | |
fn test_hare_lang() { | |
@@ -839,4 +933,16 @@ mod test { | |
fn test_wgsl_lang() { | |
assert_language(LapceLanguage::Wgsl, &["wgsl"]); | |
} | |
+ #[cfg(feature = "lang-dockerfile")] | |
+ fn test_dockerfile_lang() { | |
+ assert_language(LapceLanguage::Dockerfile, &["dockerfile"]); | |
+ } | |
+ #[cfg(feature = "lang-csharp")] | |
+ fn test_csharp_lang() { | |
+ assert_language(LapceLanguage::Csharp, &["cs", "csx"]); | |
+ } | |
+ #[cfg(feature = "lang-nix")] | |
+ fn test_nix_lang() { | |
+ assert_language(LapceLanguage::Nix, &["nix"]); | |
+ } | |
} | |
diff --git a/lapce-core/src/style.rs b/lapce-core/src/style.rs | |
index 12bb829cb..0723ca00e 100644 | |
--- a/lapce-core/src/style.rs | |
+++ b/lapce-core/src/style.rs | |
@@ -1,19 +1,9 @@ | |
-use std::{ | |
- iter, mem, ops, str, | |
- sync::atomic::{AtomicUsize, Ordering}, | |
-}; | |
+use std::str; | |
use lapce_rpc::style::{LineStyle, Style}; | |
-use thiserror::Error; | |
-use tree_sitter::{ | |
- Language, LossyUtf8, Node, Point, Query, QueryCaptures, QueryCursor, QueryError, | |
- QueryMatch, Range, Tree, | |
-}; | |
+ | |
use xi_rope::{spans::Spans, LinesMetric, Rope}; | |
-const CANCELLATION_CHECK_INTERVAL: usize = 100; | |
-const BUFFER_HTML_RESERVE_CAPACITY: usize = 10 * 1024; | |
-const BUFFER_LINES_RESERVE_CAPACITY: usize = 1000; | |
pub const SCOPES: &[&str] = &[ | |
"constant", | |
"type", | |
@@ -32,923 +22,19 @@ pub const SCOPES: &[&str] = &[ | |
"escape", | |
"embedded", | |
"symbol", | |
+ "punctuation", | |
+ "punctuation.special", | |
+ "punctuation.delimiter", | |
+ "text", | |
+ "text.literal", | |
+ "text.title", | |
+ "text.uri", | |
+ "text.reference", | |
+ "string.escape", | |
+ "conceal", | |
+ "none", | |
]; | |
-/// Indicates which highlight should be applied to a region of source code. | |
-#[derive(Copy, Clone, Debug, PartialEq, Eq)] | |
-pub struct Highlight(pub usize); | |
- | |
-/// Represents the reason why syntax highlighting failed. | |
-#[derive(Debug, Error, PartialEq, Eq)] | |
-pub enum Error { | |
- #[error("Cancelled")] | |
- Cancelled, | |
- #[error("Invalid language")] | |
- InvalidLanguage, | |
- #[error("Unknown error")] | |
- Unknown, | |
-} | |
- | |
-/// Represents a single step in rendering a syntax-highlighted document. | |
-#[derive(Copy, Clone, Debug)] | |
-pub enum HighlightEvent { | |
- Source { start: usize, end: usize }, | |
- HighlightStart(Highlight), | |
- HighlightEnd, | |
-} | |
- | |
-/// Contains the data needed to highlight code written in a particular language. | |
-/// | |
-/// This struct is immutable and can be shared between threads. | |
-pub struct HighlightConfiguration { | |
- pub language: Language, | |
- pub query: Query, | |
- combined_injections_query: Option<Query>, | |
- locals_pattern_index: usize, | |
- highlights_pattern_index: usize, | |
- highlight_indices: Vec<Option<Highlight>>, | |
- non_local_variable_patterns: Vec<bool>, | |
- injection_content_capture_index: Option<u32>, | |
- injection_language_capture_index: Option<u32>, | |
- local_scope_capture_index: Option<u32>, | |
- local_def_capture_index: Option<u32>, | |
- local_def_value_capture_index: Option<u32>, | |
- local_ref_capture_index: Option<u32>, | |
-} | |
- | |
-/// Performs syntax highlighting, recognizing a given list of highlight names. | |
-/// | |
-/// For the best performance `Highlighter` values should be reused between | |
-/// syntax highlighting calls. A separate highlighter is needed for each thread that | |
-/// is performing highlighting. | |
-pub struct Highlighter { | |
- cursors: Vec<QueryCursor>, | |
-} | |
- | |
-/// Converts a general-purpose syntax highlighting iterator into a sequence of lines of HTML. | |
-pub struct HtmlRenderer { | |
- pub html: Vec<u8>, | |
- pub line_offsets: Vec<u32>, | |
- carriage_return_highlight: Option<Highlight>, | |
-} | |
- | |
-#[derive(Debug)] | |
-struct LocalDef<'a> { | |
- name: &'a str, | |
- value_range: ops::Range<usize>, | |
- highlight: Option<Highlight>, | |
-} | |
- | |
-#[derive(Debug)] | |
-struct LocalScope<'a> { | |
- inherits: bool, | |
- range: ops::Range<usize>, | |
- local_defs: Vec<LocalDef<'a>>, | |
-} | |
- | |
-struct HighlightIter<'a, F> | |
-where | |
- F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, | |
-{ | |
- tree: Tree, | |
- source: &'a [u8], | |
- byte_offset: usize, | |
- highlighter: &'a mut Highlighter, | |
- injection_callback: F, | |
- cancellation_flag: Option<&'a AtomicUsize>, | |
- layers: Vec<HighlightIterLayer<'a>>, | |
- iter_count: usize, | |
- next_event: Option<HighlightEvent>, | |
- last_highlight_range: Option<(usize, usize, usize)>, | |
-} | |
- | |
-struct HighlightIterLayer<'a> { | |
- _tree: Tree, | |
- cursor: QueryCursor, | |
- captures: iter::Peekable<QueryCaptures<'a, 'a, &'a [u8]>>, | |
- config: &'a HighlightConfiguration, | |
- highlight_end_stack: Vec<usize>, | |
- scope_stack: Vec<LocalScope<'a>>, | |
- ranges: Vec<Range>, | |
- depth: usize, | |
-} | |
- | |
-impl Highlight { | |
- pub fn str(&self) -> &str { | |
- SCOPES[self.0] | |
- } | |
-} | |
- | |
-impl Highlighter { | |
- pub fn new() -> Self { | |
- Highlighter { | |
- cursors: Vec::new(), | |
- } | |
- } | |
- | |
- /// Iterate over the highlighted regions for a given slice of source code. | |
- pub fn highlight<'a>( | |
- &'a mut self, | |
- tree: Tree, | |
- config: &'a HighlightConfiguration, | |
- source: &'a [u8], | |
- cancellation_flag: Option<&'a AtomicUsize>, | |
- mut injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> | |
- + 'a, | |
- ) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a { | |
- let layers = HighlightIterLayer::new( | |
- tree.clone(), | |
- source, | |
- self, | |
- &mut injection_callback, | |
- config, | |
- 0, | |
- vec![Range { | |
- start_byte: 0, | |
- end_byte: usize::MAX, | |
- start_point: Point::new(0, 0), | |
- end_point: Point::new(usize::MAX, usize::MAX), | |
- }], | |
- ); | |
- assert_ne!(layers.len(), 0); | |
- let mut result = HighlightIter { | |
- tree, | |
- source, | |
- byte_offset: 0, | |
- injection_callback, | |
- cancellation_flag, | |
- highlighter: self, | |
- iter_count: 0, | |
- layers, | |
- next_event: None, | |
- last_highlight_range: None, | |
- }; | |
- result.sort_layers(); | |
- result | |
- } | |
-} | |
- | |
-impl Default for Highlighter { | |
- fn default() -> Self { | |
- Self::new() | |
- } | |
-} | |
- | |
-impl HighlightConfiguration { | |
- /// Creates a `HighlightConfiguration` for a given `Language` and set of highlighting | |
- /// queries. | |
- /// | |
- /// # Parameters | |
- /// | |
- /// * `language` - The Tree-sitter `Language` that should be used for parsing. | |
- /// * `highlights_query` - A string containing tree patterns for syntax highlighting. This | |
- /// should be non-empty, otherwise no syntax highlights will be added. | |
- /// * `injections_query` - A string containing tree patterns for injecting other languages | |
- /// into the document. This can be empty if no injections are desired. | |
- /// * `locals_query` - A string containing tree patterns for tracking local variable | |
- /// definitions and references. This can be empty if local variable tracking is not needed. | |
- /// | |
- /// Returns a `HighlightConfiguration` that can then be used with the `highlight` method. | |
- pub fn new( | |
- language: Language, | |
- highlights_query: &str, | |
- injection_query: &str, | |
- locals_query: &str, | |
- ) -> Result<Self, QueryError> { | |
- // Concatenate the query strings, keeping track of the start offset of each section. | |
- let mut query_source = String::new(); | |
- query_source.push_str(injection_query); | |
- let locals_query_offset = query_source.len(); | |
- query_source.push_str(locals_query); | |
- let highlights_query_offset = query_source.len(); | |
- query_source.push_str(highlights_query); | |
- | |
- // Construct a single query by concatenating the three query strings, but record the | |
- // range of pattern indices that belong to each individual string. | |
- let mut query = Query::new(language, &query_source)?; | |
- let mut locals_pattern_index = 0; | |
- let mut highlights_pattern_index = 0; | |
- for i in 0..(query.pattern_count()) { | |
- let pattern_offset = query.start_byte_for_pattern(i); | |
- if pattern_offset < highlights_query_offset { | |
- if pattern_offset < highlights_query_offset { | |
- highlights_pattern_index += 1; | |
- } | |
- if pattern_offset < locals_query_offset { | |
- locals_pattern_index += 1; | |
- } | |
- } | |
- } | |
- | |
- // Construct a separate query just for dealing with the 'combined injections'. | |
- // Disable the combined injection patterns in the main query. | |
- let mut combined_injections_query = Query::new(language, injection_query)?; | |
- let mut has_combined_queries = false; | |
- for pattern_index in 0..locals_pattern_index { | |
- let settings = query.property_settings(pattern_index); | |
- if settings.iter().any(|s| &*s.key == "injection.combined") { | |
- has_combined_queries = true; | |
- query.disable_pattern(pattern_index); | |
- } else { | |
- combined_injections_query.disable_pattern(pattern_index); | |
- } | |
- } | |
- let combined_injections_query = if has_combined_queries { | |
- Some(combined_injections_query) | |
- } else { | |
- None | |
- }; | |
- | |
- // Find all of the highlighting patterns that are disabled for nodes that | |
- // have been identified as local variables. | |
- let non_local_variable_patterns = (0..query.pattern_count()) | |
- .map(|i| { | |
- query.property_predicates(i).iter().any(|(prop, positive)| { | |
- !*positive && prop.key.as_ref() == "local" | |
- }) | |
- }) | |
- .collect(); | |
- | |
- // Store the numeric ids for all of the special captures. | |
- let mut injection_content_capture_index = None; | |
- let mut injection_language_capture_index = None; | |
- let mut local_def_capture_index = None; | |
- let mut local_def_value_capture_index = None; | |
- let mut local_ref_capture_index = None; | |
- let mut local_scope_capture_index = None; | |
- for (i, name) in query.capture_names().iter().enumerate() { | |
- let i = Some(i as u32); | |
- match name.as_str() { | |
- "injection.content" => injection_content_capture_index = i, | |
- "injection.language" => injection_language_capture_index = i, | |
- "local.definition" => local_def_capture_index = i, | |
- "local.definition-value" => local_def_value_capture_index = i, | |
- "local.reference" => local_ref_capture_index = i, | |
- "local.scope" => local_scope_capture_index = i, | |
- _ => {} | |
- } | |
- } | |
- | |
- let highlight_indices = vec![None; query.capture_names().len()]; | |
- let mut conf = HighlightConfiguration { | |
- language, | |
- query, | |
- combined_injections_query, | |
- locals_pattern_index, | |
- highlights_pattern_index, | |
- highlight_indices, | |
- non_local_variable_patterns, | |
- injection_content_capture_index, | |
- injection_language_capture_index, | |
- local_def_capture_index, | |
- local_def_value_capture_index, | |
- local_ref_capture_index, | |
- local_scope_capture_index, | |
- }; | |
- conf.configure(SCOPES); | |
- Ok(conf) | |
- } | |
- | |
- /// Get a slice containing all of the highlight names used in the configuration. | |
- pub fn names(&self) -> &[String] { | |
- self.query.capture_names() | |
- } | |
- | |
- /// Set the list of recognized highlight names. | |
- /// | |
- /// Tree-sitter syntax-highlighting queries specify highlights in the form of dot-separated | |
- /// highlight names like `punctuation.bracket` and `function.method.builtin`. Consumers of | |
- /// these queries can choose to recognize highlights with different levels of specificity. | |
- /// For example, the string `function.builtin` will match against `function.method.builtin` | |
- /// and `function.builtin.constructor`, but will not match `function.method`. | |
- /// | |
- /// When highlighting, results are returned as `Highlight` values, which contain the index | |
- /// of the matched highlight this list of highlight names. | |
- fn configure(&mut self, recognized_names: &[impl AsRef<str>]) { | |
- let mut capture_parts = Vec::new(); | |
- self.highlight_indices.clear(); | |
- self.highlight_indices | |
- .extend(self.query.capture_names().iter().map(move |capture_name| { | |
- capture_parts.clear(); | |
- capture_parts.extend(capture_name.split('.')); | |
- | |
- let mut best_index = None; | |
- let mut best_match_len = 0; | |
- for (i, recognized_name) in recognized_names.iter().enumerate() { | |
- let mut len = 0; | |
- let mut matches = true; | |
- for part in recognized_name.as_ref().split('.') { | |
- len += 1; | |
- if !capture_parts.contains(&part) { | |
- matches = false; | |
- break; | |
- } | |
- } | |
- if matches && len > best_match_len { | |
- best_index = Some(i); | |
- best_match_len = len; | |
- } | |
- } | |
- best_index.map(Highlight) | |
- })); | |
- } | |
-} | |
- | |
-impl<'a> HighlightIterLayer<'a> { | |
- /// Create a new 'layer' of highlighting for this document. | |
- /// | |
- /// In the even that the new layer contains "combined injections" (injections where multiple | |
- /// disjoint ranges are parsed as one syntax tree), these will be eagerly processed and | |
- /// added to the returned vector. | |
- fn new<F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a>( | |
- tree: Tree, | |
- source: &'a [u8], | |
- highlighter: &mut Highlighter, | |
- injection_callback: &mut F, | |
- mut config: &'a HighlightConfiguration, | |
- mut depth: usize, | |
- mut ranges: Vec<Range>, | |
- ) -> Vec<Self> { | |
- let mut result = Vec::with_capacity(1); | |
- let mut queue = Vec::new(); | |
- loop { | |
- let tree = tree.clone(); | |
- let mut cursor = | |
- highlighter.cursors.pop().unwrap_or_else(QueryCursor::new); | |
- | |
- // Process combined injections. | |
- if let Some(combined_injections_query) = | |
- &config.combined_injections_query | |
- { | |
- let mut injections_by_pattern_index = vec![ | |
- (None, Vec::new(), false); | |
- combined_injections_query | |
- .pattern_count() | |
- ]; | |
- let matches = cursor.matches( | |
- combined_injections_query, | |
- tree.root_node(), | |
- source, | |
- ); | |
- for mat in matches { | |
- let entry = &mut injections_by_pattern_index[mat.pattern_index]; | |
- let (language_name, content_node, include_children) = | |
- injection_for_match( | |
- config, | |
- combined_injections_query, | |
- &mat, | |
- source, | |
- ); | |
- if language_name.is_some() { | |
- entry.0 = language_name; | |
- } | |
- if let Some(content_node) = content_node { | |
- entry.1.push(content_node); | |
- } | |
- entry.2 = include_children; | |
- } | |
- for (lang_name, content_nodes, includes_children) in | |
- injections_by_pattern_index | |
- { | |
- if let (Some(lang_name), false) = | |
- (lang_name, content_nodes.is_empty()) | |
- { | |
- if let Some(next_config) = (injection_callback)(lang_name) { | |
- let ranges = Self::intersect_ranges( | |
- &ranges, | |
- &content_nodes, | |
- includes_children, | |
- ); | |
- if !ranges.is_empty() { | |
- queue.push((next_config, depth + 1, ranges)); | |
- } | |
- } | |
- } | |
- } | |
- } | |
- | |
- // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which | |
- // prevents them from being moved. But both of these values are really just | |
- // pointers, so it's actually ok to move them. | |
- let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(&tree) }; | |
- let cursor_ref = unsafe { | |
- mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) | |
- }; | |
- let captures = cursor_ref | |
- .captures(&config.query, tree_ref.root_node(), source) | |
- .peekable(); | |
- | |
- result.push(HighlightIterLayer { | |
- highlight_end_stack: Vec::new(), | |
- scope_stack: vec![LocalScope { | |
- inherits: false, | |
- range: 0..usize::MAX, | |
- local_defs: Vec::new(), | |
- }], | |
- cursor, | |
- depth, | |
- _tree: tree, | |
- captures, | |
- config, | |
- ranges, | |
- }); | |
- | |
- if queue.is_empty() { | |
- break; | |
- } else { | |
- let (next_config, next_depth, next_ranges) = queue.remove(0); | |
- config = next_config; | |
- depth = next_depth; | |
- ranges = next_ranges; | |
- } | |
- } | |
- | |
- result | |
- } | |
- | |
- // Compute the ranges that should be included when parsing an injection. | |
- // This takes into account three things: | |
- // * `parent_ranges` - The ranges must all fall within the *current* layer's ranges. | |
- // * `nodes` - Every injection takes place within a set of nodes. The injection ranges | |
- // are the ranges of those nodes. | |
- // * `includes_children` - For some injections, the content nodes' children should be | |
- // excluded from the nested document, so that only the content nodes' *own* content | |
- // is reparsed. For other injections, the content nodes' entire ranges should be | |
- // reparsed, including the ranges of their children. | |
- fn intersect_ranges( | |
- parent_ranges: &[Range], | |
- nodes: &[Node], | |
- includes_children: bool, | |
- ) -> Vec<Range> { | |
- let mut cursor = nodes[0].walk(); | |
- let mut result = Vec::new(); | |
- let mut parent_range_iter = parent_ranges.iter(); | |
- let mut parent_range = parent_range_iter.next().expect( | |
- "Layers should only be constructed with non-empty ranges vectors", | |
- ); | |
- for node in nodes.iter() { | |
- let mut preceding_range = Range { | |
- start_byte: 0, | |
- start_point: Point::new(0, 0), | |
- end_byte: node.start_byte(), | |
- end_point: node.start_position(), | |
- }; | |
- let following_range = Range { | |
- start_byte: node.end_byte(), | |
- start_point: node.end_position(), | |
- end_byte: usize::MAX, | |
- end_point: Point::new(usize::MAX, usize::MAX), | |
- }; | |
- | |
- for excluded_range in node | |
- .children(&mut cursor) | |
- .filter_map(|child| { | |
- if includes_children { | |
- None | |
- } else { | |
- Some(child.range()) | |
- } | |
- }) | |
- .chain([following_range].iter().cloned()) | |
- { | |
- let mut range = Range { | |
- start_byte: preceding_range.end_byte, | |
- start_point: preceding_range.end_point, | |
- end_byte: excluded_range.start_byte, | |
- end_point: excluded_range.start_point, | |
- }; | |
- preceding_range = excluded_range; | |
- | |
- if range.end_byte < parent_range.start_byte { | |
- continue; | |
- } | |
- | |
- while parent_range.start_byte <= range.end_byte { | |
- if parent_range.end_byte > range.start_byte { | |
- if range.start_byte < parent_range.start_byte { | |
- range.start_byte = parent_range.start_byte; | |
- range.start_point = parent_range.start_point; | |
- } | |
- | |
- if parent_range.end_byte < range.end_byte { | |
- if range.start_byte < parent_range.end_byte { | |
- result.push(Range { | |
- start_byte: range.start_byte, | |
- start_point: range.start_point, | |
- end_byte: parent_range.end_byte, | |
- end_point: parent_range.end_point, | |
- }); | |
- } | |
- range.start_byte = parent_range.end_byte; | |
- range.start_point = parent_range.end_point; | |
- } else { | |
- if range.start_byte < range.end_byte { | |
- result.push(range); | |
- } | |
- break; | |
- } | |
- } | |
- | |
- if let Some(next_range) = parent_range_iter.next() { | |
- parent_range = next_range; | |
- } else { | |
- return result; | |
- } | |
- } | |
- } | |
- } | |
- result | |
- } | |
- | |
- // First, sort scope boundaries by their byte offset in the document. At a | |
- // given position, emit scope endings before scope beginnings. Finally, emit | |
- // scope boundaries from deeper layers first. | |
- fn sort_key(&mut self) -> Option<(usize, bool, isize)> { | |
- let depth = -(self.depth as isize); | |
- let next_start = self | |
- .captures | |
- .peek() | |
- .map(|(m, i)| m.captures[*i].node.start_byte()); | |
- let next_end = self.highlight_end_stack.last().cloned(); | |
- match (next_start, next_end) { | |
- (Some(start), Some(end)) => { | |
- if start < end { | |
- Some((start, true, depth)) | |
- } else { | |
- Some((end, false, depth)) | |
- } | |
- } | |
- (Some(i), None) => Some((i, true, depth)), | |
- (None, Some(j)) => Some((j, false, depth)), | |
- _ => None, | |
- } | |
- } | |
-} | |
- | |
-impl<'a, F> HighlightIter<'a, F> | |
-where | |
- F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, | |
-{ | |
- fn emit_event( | |
- &mut self, | |
- offset: usize, | |
- event: Option<HighlightEvent>, | |
- ) -> Option<Result<HighlightEvent, Error>> { | |
- let result; | |
- if self.byte_offset < offset { | |
- result = Some(Ok(HighlightEvent::Source { | |
- start: self.byte_offset, | |
- end: offset, | |
- })); | |
- self.byte_offset = offset; | |
- self.next_event = event; | |
- } else { | |
- result = event.map(Ok); | |
- } | |
- self.sort_layers(); | |
- result | |
- } | |
- | |
- fn sort_layers(&mut self) { | |
- while !self.layers.is_empty() { | |
- if let Some(sort_key) = self.layers[0].sort_key() { | |
- let mut i = 0; | |
- while i + 1 < self.layers.len() { | |
- if let Some(next_offset) = self.layers[i + 1].sort_key() { | |
- if next_offset < sort_key { | |
- i += 1; | |
- continue; | |
- } | |
- } | |
- break; | |
- } | |
- if i > 0 { | |
- self.layers[0..(i + 1)].rotate_left(1); | |
- } | |
- break; | |
- } else { | |
- let layer = self.layers.remove(0); | |
- self.highlighter.cursors.push(layer.cursor); | |
- } | |
- } | |
- } | |
- | |
- fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) { | |
- if let Some(sort_key) = layer.sort_key() { | |
- let mut i = 1; | |
- while i < self.layers.len() { | |
- if let Some(sort_key_i) = self.layers[i].sort_key() { | |
- if sort_key_i > sort_key { | |
- self.layers.insert(i, layer); | |
- return; | |
- } | |
- i += 1; | |
- } else { | |
- self.layers.remove(i); | |
- } | |
- } | |
- self.layers.push(layer); | |
- } | |
- } | |
-} | |
- | |
-impl<'a, F> Iterator for HighlightIter<'a, F> | |
-where | |
- F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, | |
-{ | |
- type Item = Result<HighlightEvent, Error>; | |
- | |
- fn next(&mut self) -> Option<Self::Item> { | |
- 'main: loop { | |
- // If we've already determined the next highlight boundary, just return it. | |
- if let Some(e) = self.next_event.take() { | |
- return Some(Ok(e)); | |
- } | |
- | |
- // Periodically check for cancellation, returning `Cancelled` error if the | |
- // cancellation flag was flipped. | |
- if let Some(cancellation_flag) = self.cancellation_flag { | |
- self.iter_count += 1; | |
- if self.iter_count >= CANCELLATION_CHECK_INTERVAL { | |
- self.iter_count = 0; | |
- if cancellation_flag.load(Ordering::Relaxed) != 0 { | |
- return Some(Err(Error::Cancelled)); | |
- } | |
- } | |
- } | |
- | |
- // If none of the layers have any more highlight boundaries, terminate. | |
- if self.layers.is_empty() { | |
- return if self.byte_offset < self.source.len() { | |
- let result = Some(Ok(HighlightEvent::Source { | |
- start: self.byte_offset, | |
- end: self.source.len(), | |
- })); | |
- self.byte_offset = self.source.len(); | |
- result | |
- } else { | |
- None | |
- }; | |
- } | |
- | |
- // Get the next capture from whichever layer has the earliest highlight boundary. | |
- let range; | |
- let layer = &mut self.layers[0]; | |
- if let Some((next_match, capture_index)) = layer.captures.peek() { | |
- let next_capture = next_match.captures[*capture_index]; | |
- range = next_capture.node.byte_range(); | |
- | |
- // If any previous highlight ends before this node starts, then before | |
- // processing this capture, emit the source code up until the end of the | |
- // previous highlight, and an end event for that highlight. | |
- if let Some(end_byte) = layer.highlight_end_stack.last().cloned() { | |
- if end_byte <= range.start { | |
- layer.highlight_end_stack.pop(); | |
- return self.emit_event( | |
- end_byte, | |
- Some(HighlightEvent::HighlightEnd), | |
- ); | |
- } | |
- } | |
- } | |
- // If there are no more captures, then emit any remaining highlight end events. | |
- // And if there are none of those, then just advance to the end of the document. | |
- else if let Some(end_byte) = layer.highlight_end_stack.last().cloned() | |
- { | |
- layer.highlight_end_stack.pop(); | |
- return self | |
- .emit_event(end_byte, Some(HighlightEvent::HighlightEnd)); | |
- } else { | |
- return self.emit_event(self.source.len(), None); | |
- }; | |
- | |
- let (mut match_, capture_index) = layer.captures.next().unwrap(); | |
- let mut capture = match_.captures[capture_index]; | |
- | |
- // If this capture represents an injection, then process the injection. | |
- if match_.pattern_index < layer.config.locals_pattern_index { | |
- let (language_name, content_node, include_children) = | |
- injection_for_match( | |
- layer.config, | |
- &layer.config.query, | |
- &match_, | |
- self.source, | |
- ); | |
- | |
- // Explicitly remove this match so that none of its other captures will remain | |
- // in the stream of captures. | |
- match_.remove(); | |
- | |
- // If a language is found with the given name, then add a new language layer | |
- // to the highlighted document. | |
- if let (Some(language_name), Some(content_node)) = | |
- (language_name, content_node) | |
- { | |
- if let Some(config) = (self.injection_callback)(language_name) { | |
- let ranges = HighlightIterLayer::intersect_ranges( | |
- &self.layers[0].ranges, | |
- &[content_node], | |
- include_children, | |
- ); | |
- if !ranges.is_empty() { | |
- for layer in HighlightIterLayer::new( | |
- self.tree.clone(), | |
- self.source, | |
- self.highlighter, | |
- &mut self.injection_callback, | |
- config, | |
- self.layers[0].depth + 1, | |
- ranges, | |
- ) { | |
- self.insert_layer(layer); | |
- } | |
- } | |
- } | |
- } | |
- | |
- self.sort_layers(); | |
- continue 'main; | |
- } | |
- | |
- // Remove from the local scope stack any local scopes that have already ended. | |
- while range.start > layer.scope_stack.last().unwrap().range.end { | |
- layer.scope_stack.pop(); | |
- } | |
- | |
- // If this capture is for tracking local variables, then process the | |
- // local variable info. | |
- let mut reference_highlight = None; | |
- let mut definition_highlight = None; | |
- while match_.pattern_index < layer.config.highlights_pattern_index { | |
- // If the node represents a local scope, push a new local scope onto | |
- // the scope stack. | |
- if Some(capture.index) == layer.config.local_scope_capture_index { | |
- definition_highlight = None; | |
- let mut scope = LocalScope { | |
- inherits: true, | |
- range: range.clone(), | |
- local_defs: Vec::new(), | |
- }; | |
- for prop in | |
- layer.config.query.property_settings(match_.pattern_index) | |
- { | |
- if prop.key.as_ref() == "local.scope-inherits" { | |
- scope.inherits = prop | |
- .value | |
- .as_ref() | |
- .map_or(true, |r| r.as_ref() == "true"); | |
- } | |
- } | |
- layer.scope_stack.push(scope); | |
- } | |
- // If the node represents a definition, add a new definition to the | |
- // local scope at the top of the scope stack. | |
- else if Some(capture.index) == layer.config.local_def_capture_index | |
- { | |
- reference_highlight = None; | |
- definition_highlight = None; | |
- let scope = layer.scope_stack.last_mut().unwrap(); | |
- | |
- let mut value_range = 0..0; | |
- for capture in match_.captures { | |
- if Some(capture.index) | |
- == layer.config.local_def_value_capture_index | |
- { | |
- value_range = capture.node.byte_range(); | |
- } | |
- } | |
- | |
- if let Ok(name) = str::from_utf8(&self.source[range.clone()]) { | |
- scope.local_defs.push(LocalDef { | |
- name, | |
- value_range, | |
- highlight: None, | |
- }); | |
- definition_highlight = | |
- scope.local_defs.last_mut().map(|s| &mut s.highlight); | |
- } | |
- } | |
- // If the node represents a reference, then try to find the corresponding | |
- // definition in the scope stack. | |
- else if Some(capture.index) == layer.config.local_ref_capture_index | |
- && definition_highlight.is_none() | |
- { | |
- definition_highlight = None; | |
- if let Ok(name) = str::from_utf8(&self.source[range.clone()]) { | |
- for scope in layer.scope_stack.iter().rev() { | |
- if let Some(highlight) = | |
- scope.local_defs.iter().rev().find_map(|def| { | |
- if def.name == name | |
- && range.start >= def.value_range.end | |
- { | |
- Some(def.highlight) | |
- } else { | |
- None | |
- } | |
- }) | |
- { | |
- reference_highlight = highlight; | |
- break; | |
- } | |
- if !scope.inherits { | |
- break; | |
- } | |
- } | |
- } | |
- } | |
- | |
- // Continue processing any additional matches for the same node. | |
- if let Some((next_match, next_capture_index)) = layer.captures.peek() | |
- { | |
- let next_capture = next_match.captures[*next_capture_index]; | |
- if next_capture.node == capture.node { | |
- capture = next_capture; | |
- match_ = layer.captures.next().unwrap().0; | |
- continue; | |
- } | |
- } | |
- | |
- self.sort_layers(); | |
- continue 'main; | |
- } | |
- | |
- // Otherwise, this capture must represent a highlight. | |
- // If this exact range has already been highlighted by an earlier pattern, or by | |
- // a different layer, then skip over this one. | |
- if let Some((last_start, last_end, last_depth)) = | |
- self.last_highlight_range | |
- { | |
- if range.start == last_start | |
- && range.end == last_end | |
- && layer.depth < last_depth | |
- { | |
- self.sort_layers(); | |
- continue 'main; | |
- } | |
- } | |
- | |
- // If the current node was found to be a local variable, then skip over any | |
- // highlighting patterns that are disabled for local variables. | |
- if definition_highlight.is_some() || reference_highlight.is_some() { | |
- while layer.config.non_local_variable_patterns[match_.pattern_index] | |
- { | |
- match_.remove(); | |
- if let Some((next_match, next_capture_index)) = | |
- layer.captures.peek() | |
- { | |
- let next_capture = next_match.captures[*next_capture_index]; | |
- if next_capture.node == capture.node { | |
- capture = next_capture; | |
- match_ = layer.captures.next().unwrap().0; | |
- continue; | |
- } | |
- } | |
- | |
- self.sort_layers(); | |
- continue 'main; | |
- } | |
- } | |
- | |
- // Once a highlighting pattern is found for the current node, skip over | |
- // any later highlighting patterns that also match this node. Captures | |
- // for a given node are ordered by pattern index, so these subsequent | |
- // captures are guaranteed to be for highlighting, not injections or | |
- // local variables. | |
- while let Some((next_match, next_capture_index)) = layer.captures.peek() | |
- { | |
- let next_capture = next_match.captures[*next_capture_index]; | |
- if next_capture.node == capture.node { | |
- layer.captures.next(); | |
- } else { | |
- break; | |
- } | |
- } | |
- | |
- let current_highlight = | |
- layer.config.highlight_indices[capture.index as usize]; | |
- | |
- // If this node represents a local definition, then store the current | |
- // highlight value on the local scope entry representing this node. | |
- if let Some(definition_highlight) = definition_highlight { | |
- *definition_highlight = current_highlight; | |
- } | |
- | |
- // Emit a scope start event and push the node's end position to the stack. | |
- if let Some(highlight) = reference_highlight.or(current_highlight) { | |
- self.last_highlight_range = | |
- Some((range.start, range.end, layer.depth)); | |
- layer.highlight_end_stack.push(range.end); | |
- return self.emit_event( | |
- range.start, | |
- Some(HighlightEvent::HighlightStart(highlight)), | |
- ); | |
- } | |
- | |
- self.sort_layers(); | |
- } | |
- } | |
-} | |
- | |
pub fn line_styles( | |
text: &Rope, | |
line: usize, | |
@@ -983,218 +69,3 @@ pub fn line_styles( | |
.collect(); | |
line_styles | |
} | |
- | |
-impl HtmlRenderer { | |
- pub fn new() -> Self { | |
- let mut result = HtmlRenderer { | |
- html: Vec::with_capacity(BUFFER_HTML_RESERVE_CAPACITY), | |
- line_offsets: Vec::with_capacity(BUFFER_LINES_RESERVE_CAPACITY), | |
- carriage_return_highlight: None, | |
- }; | |
- result.line_offsets.push(0); | |
- result | |
- } | |
- | |
- pub fn set_carriage_return_highlight(&mut self, highlight: Option<Highlight>) { | |
- self.carriage_return_highlight = highlight; | |
- } | |
- | |
- pub fn reset(&mut self) { | |
- shrink_and_clear(&mut self.html, BUFFER_HTML_RESERVE_CAPACITY); | |
- shrink_and_clear(&mut self.line_offsets, BUFFER_LINES_RESERVE_CAPACITY); | |
- self.line_offsets.push(0); | |
- } | |
- | |
- pub fn render<'a, F>( | |
- &mut self, | |
- highlighter: impl Iterator<Item = Result<HighlightEvent, Error>>, | |
- source: &'a [u8], | |
- attribute_callback: &F, | |
- ) -> Result<(), Error> | |
- where | |
- F: Fn(Highlight) -> &'a [u8], | |
- { | |
- let mut highlights = Vec::new(); | |
- for event in highlighter { | |
- match event { | |
- Ok(HighlightEvent::HighlightStart(s)) => { | |
- highlights.push(s); | |
- self.start_highlight(s, attribute_callback); | |
- } | |
- Ok(HighlightEvent::HighlightEnd) => { | |
- highlights.pop(); | |
- self.end_highlight(); | |
- } | |
- Ok(HighlightEvent::Source { start, end }) => { | |
- self.add_text( | |
- &source[start..end], | |
- &highlights, | |
- attribute_callback, | |
- ); | |
- } | |
- Err(a) => return Err(a), | |
- } | |
- } | |
- if self.html.last() != Some(&b'\n') { | |
- self.html.push(b'\n'); | |
- } | |
- if self.line_offsets.last() == Some(&(self.html.len() as u32)) { | |
- self.line_offsets.pop(); | |
- } | |
- Ok(()) | |
- } | |
- | |
- pub fn lines(&self) -> impl Iterator<Item = &str> { | |
- self.line_offsets | |
- .iter() | |
- .enumerate() | |
- .map(move |(i, line_start)| { | |
- let line_start = *line_start as usize; | |
- let line_end = if i + 1 == self.line_offsets.len() { | |
- self.html.len() | |
- } else { | |
- self.line_offsets[i + 1] as usize | |
- }; | |
- str::from_utf8(&self.html[line_start..line_end]).unwrap() | |
- }) | |
- } | |
- | |
- fn add_carriage_return<'a, F>(&mut self, attribute_callback: &F) | |
- where | |
- F: Fn(Highlight) -> &'a [u8], | |
- { | |
- if let Some(highlight) = self.carriage_return_highlight { | |
- let attribute_string = (attribute_callback)(highlight); | |
- if !attribute_string.is_empty() { | |
- self.html.extend(b"<span "); | |
- self.html.extend(attribute_string); | |
- self.html.extend(b"></span>"); | |
- } | |
- } | |
- } | |
- | |
- fn start_highlight<'a, F>(&mut self, h: Highlight, attribute_callback: &F) | |
- where | |
- F: Fn(Highlight) -> &'a [u8], | |
- { | |
- let attribute_string = (attribute_callback)(h); | |
- self.html.extend(b"<span"); | |
- if !attribute_string.is_empty() { | |
- self.html.extend(b" "); | |
- self.html.extend(attribute_string); | |
- } | |
- self.html.extend(b">"); | |
- } | |
- | |
- fn end_highlight(&mut self) { | |
- self.html.extend(b"</span>"); | |
- } | |
- | |
- fn add_text<'a, F>( | |
- &mut self, | |
- src: &[u8], | |
- highlights: &[Highlight], | |
- attribute_callback: &F, | |
- ) where | |
- F: Fn(Highlight) -> &'a [u8], | |
- { | |
- let mut last_char_was_cr = false; | |
- for c in LossyUtf8::new(src).flat_map(|p| p.bytes()) { | |
- // Don't render carriage return characters, but allow lone carriage returns (not | |
- // followed by line feeds) to be styled via the attribute callback. | |
- if c == b'\r' { | |
- last_char_was_cr = true; | |
- continue; | |
- } | |
- if last_char_was_cr { | |
- if c != b'\n' { | |
- self.add_carriage_return(attribute_callback); | |
- } | |
- last_char_was_cr = false; | |
- } | |
- | |
- // At line boundaries, close and re-open all of the open tags. | |
- if c == b'\n' { | |
- highlights.iter().for_each(|_| self.end_highlight()); | |
- self.html.push(c); | |
- self.line_offsets.push(self.html.len() as u32); | |
- highlights.iter().for_each(|scope| { | |
- self.start_highlight(*scope, attribute_callback) | |
- }); | |
- } else if let Some(escape) = html_escape(c) { | |
- self.html.extend_from_slice(escape); | |
- } else { | |
- self.html.push(c); | |
- } | |
- } | |
- } | |
-} | |
- | |
-impl Default for HtmlRenderer { | |
- fn default() -> Self { | |
- Self::new() | |
- } | |
-} | |
- | |
-fn injection_for_match<'a>( | |
- config: &HighlightConfiguration, | |
- query: &'a Query, | |
- query_match: &QueryMatch<'a, 'a>, | |
- source: &'a [u8], | |
-) -> (Option<&'a str>, Option<Node<'a>>, bool) { | |
- let content_capture_index = config.injection_content_capture_index; | |
- let language_capture_index = config.injection_language_capture_index; | |
- | |
- let mut language_name = None; | |
- let mut content_node = None; | |
- for capture in query_match.captures { | |
- let index = Some(capture.index); | |
- if index == language_capture_index { | |
- language_name = capture.node.utf8_text(source).ok(); | |
- } else if index == content_capture_index { | |
- content_node = Some(capture.node); | |
- } | |
- } | |
- | |
- let mut include_children = false; | |
- for prop in query.property_settings(query_match.pattern_index) { | |
- match prop.key.as_ref() { | |
- // In addition to specifying the language name via the text of a | |
- // captured node, it can also be hard-coded via a `#set!` predicate | |
- // that sets the injection.language key. | |
- "injection.language" => { | |
- if language_name.is_none() { | |
- language_name = prop.value.as_ref().map(|s| s.as_ref()) | |
- } | |
- } | |
- | |
- // By default, injections do not include the *children* of an | |
- // `injection.content` node - only the ranges that belong to the | |
- // node itself. This can be changed using a `#set!` predicate that | |
- // sets the `injection.include-children` key. | |
- "injection.include-children" => include_children = true, | |
- _ => {} | |
- } | |
- } | |
- | |
- (language_name, content_node, include_children) | |
-} | |
- | |
-fn shrink_and_clear<T>(vec: &mut Vec<T>, capacity: usize) { | |
- if vec.len() > capacity { | |
- vec.truncate(capacity); | |
- vec.shrink_to_fit(); | |
- } | |
- vec.clear(); | |
-} | |
- | |
-fn html_escape(c: u8) -> Option<&'static [u8]> { | |
- match c as char { | |
- '>' => Some(b">"), | |
- '<' => Some(b"<"), | |
- '&' => Some(b"&"), | |
- '\'' => Some(b"'"), | |
- '"' => Some(b"""), | |
- _ => None, | |
- } | |
-} | |
diff --git a/lapce-core/src/syntax.rs b/lapce-core/src/syntax.rs | |
deleted file mode 100644 | |
index 009d3a701..000000000 | |
--- a/lapce-core/src/syntax.rs | |
+++ /dev/null | |
@@ -1,476 +0,0 @@ | |
-use std::{ | |
- cell::RefCell, | |
- collections::{HashMap, HashSet}, | |
- path::Path, | |
- sync::Arc, | |
-}; | |
- | |
-use itertools::Itertools; | |
-use lapce_rpc::style::Style; | |
-use tree_sitter::{Node, Parser, Point, Tree}; | |
-use xi_rope::{ | |
- spans::{Spans, SpansBuilder}, | |
- Interval, Rope, RopeDelta, | |
-}; | |
- | |
-use crate::{ | |
- language::LapceLanguage, | |
- lens::{Lens, LensBuilder}, | |
- style::{Highlight, HighlightEvent, Highlighter, SCOPES}, | |
-}; | |
- | |
-thread_local! { | |
- static PARSER: RefCell<HashMap<LapceLanguage, Parser>> = RefCell::new(HashMap::new()); | |
- static HIGHLIGHTS: RefCell<HashMap<LapceLanguage, crate::style::HighlightConfiguration>> = RefCell::new(HashMap::new()); | |
-} | |
- | |
-#[derive(Clone)] | |
-pub struct Syntax { | |
- pub rev: u64, | |
- pub language: LapceLanguage, | |
- pub text: Rope, | |
- tree: Option<Tree>, | |
- pub lens: Lens, | |
- pub normal_lines: Vec<usize>, | |
- pub line_height: usize, | |
- pub lens_height: usize, | |
- pub styles: Option<Arc<Spans<Style>>>, | |
-} | |
- | |
-impl std::fmt::Debug for Syntax { | |
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
- f.debug_struct("Syntax") | |
- .field("rev", &self.rev) | |
- .field("language", &self.language) | |
- .field("text", &self.text) | |
- .field("tree", &self.tree) | |
- .field("normal_lines", &self.normal_lines) | |
- .field("line_height", &self.line_height) | |
- .field("lens_height", &self.lens_height) | |
- .field("styles", &self.styles) | |
- .finish() | |
- } | |
-} | |
- | |
-impl Syntax { | |
- pub fn init(path: &Path) -> Option<Syntax> { | |
- LapceLanguage::from_path(path).map(|l| Syntax { | |
- rev: 0, | |
- language: l, | |
- text: Rope::from(""), | |
- tree: None, | |
- lens: Self::lens_from_normal_lines(0, 0, 0, &Vec::new()), | |
- line_height: 0, | |
- lens_height: 0, | |
- normal_lines: Vec::new(), | |
- styles: None, | |
- }) | |
- } | |
- | |
- pub fn from_language(language: LapceLanguage) -> Syntax { | |
- Syntax { | |
- rev: 0, | |
- language, | |
- text: Rope::from(""), | |
- tree: None, | |
- lens: Self::lens_from_normal_lines(0, 0, 0, &Vec::new()), | |
- line_height: 0, | |
- lens_height: 0, | |
- normal_lines: Vec::new(), | |
- styles: None, | |
- } | |
- } | |
- | |
- pub fn parse( | |
- &self, | |
- new_rev: u64, | |
- new_text: Rope, | |
- delta: Option<RopeDelta>, | |
- ) -> Syntax { | |
- let mut old_tree = None; | |
- if new_rev == self.rev + 1 { | |
- if let Some(delta) = delta { | |
- fn point_at_offset(text: &Rope, offset: usize) -> Point { | |
- let line = text.line_of_offset(offset); | |
- let col = text.offset_of_line(line + 1) - offset; | |
- Point::new(line, col) | |
- } | |
- let (interval, _) = delta.summary(); | |
- let (start, end) = interval.start_end(); | |
- if let Some(inserted) = delta.as_simple_insert() { | |
- fn traverse(point: Point, text: &str) -> Point { | |
- let Point { | |
- mut row, | |
- mut column, | |
- } = point; | |
- | |
- for ch in text.chars() { | |
- if ch == '\n' { | |
- row += 1; | |
- column = 0; | |
- } else { | |
- column += 1; | |
- } | |
- } | |
- Point { row, column } | |
- } | |
- | |
- let start_position = point_at_offset(&self.text, start); | |
- | |
- let edit = tree_sitter::InputEdit { | |
- start_byte: start, | |
- old_end_byte: start, | |
- new_end_byte: start + inserted.len(), | |
- start_position, | |
- old_end_position: start_position, | |
- new_end_position: traverse( | |
- start_position, | |
- &inserted.slice_to_cow(0..inserted.len()), | |
- ), | |
- }; | |
- old_tree = self.tree.as_ref().map(|tree| { | |
- let mut tree = tree.clone(); | |
- tree.edit(&edit); | |
- tree | |
- }); | |
- } else if delta.is_simple_delete() { | |
- let start_position = point_at_offset(&self.text, start); | |
- let end_position = point_at_offset(&self.text, end); | |
- let edit = tree_sitter::InputEdit { | |
- start_byte: start, | |
- old_end_byte: end, | |
- new_end_byte: start, | |
- start_position, | |
- old_end_position: end_position, | |
- new_end_position: start_position, | |
- }; | |
- old_tree = self.tree.as_ref().map(|tree| { | |
- let mut tree = tree.clone(); | |
- tree.edit(&edit); | |
- tree | |
- }); | |
- }; | |
- } | |
- } | |
- | |
- let new_tree = PARSER.with(|parsers| { | |
- let mut parsers = parsers.borrow_mut(); | |
- parsers | |
- .entry(self.language) | |
- .or_insert_with(|| self.language.new_parser()); | |
- let parser = parsers.get_mut(&self.language).unwrap(); | |
- | |
- parser.parse_with( | |
- &mut |byte, _| { | |
- if byte <= new_text.len() { | |
- new_text | |
- .iter_chunks(byte..) | |
- .next() | |
- .map(|s| s.as_bytes()) | |
- .unwrap_or(&[]) | |
- } else { | |
- &[] | |
- } | |
- }, | |
- old_tree.as_ref(), | |
- ) | |
- }); | |
- | |
- let styles = if let Some(tree) = new_tree.as_ref() { | |
- let styles = HIGHLIGHTS.with(|configs| { | |
- let mut configs = configs.borrow_mut(); | |
- configs | |
- .entry(self.language) | |
- .or_insert_with(|| self.language.new_highlight_config()); | |
- let config = configs.get(&self.language).unwrap(); | |
- let mut current_hl: Option<Highlight> = None; | |
- let mut highlights = SpansBuilder::new(new_text.len()); | |
- let mut highlighter = Highlighter::new(); | |
- for highlight in highlighter | |
- .highlight( | |
- tree.clone(), | |
- config, | |
- new_text.slice_to_cow(0..new_text.len()).as_bytes(), | |
- None, | |
- |_| None, | |
- ) | |
- .flatten() | |
- { | |
- match highlight { | |
- HighlightEvent::Source { start, end } => { | |
- if let Some(hl) = current_hl { | |
- if let Some(hl) = SCOPES.get(hl.0) { | |
- highlights.add_span( | |
- Interval::new(start, end), | |
- Style { | |
- fg_color: Some(hl.to_string()), | |
- }, | |
- ); | |
- } | |
- } | |
- } | |
- HighlightEvent::HighlightStart(hl) => { | |
- current_hl = Some(hl); | |
- } | |
- HighlightEvent::HighlightEnd => current_hl = None, | |
- } | |
- } | |
- highlights.build() | |
- }); | |
- Some(Arc::new(styles)) | |
- } else { | |
- None | |
- }; | |
- | |
- let normal_lines = if let Some(tree) = new_tree.as_ref() { | |
- let mut cursor = tree.walk(); | |
- let mut normal_lines = HashSet::new(); | |
- self.language.walk_tree(&mut cursor, &mut normal_lines); | |
- let normal_lines: Vec<usize> = | |
- normal_lines.into_iter().sorted().collect(); | |
- normal_lines | |
- } else { | |
- Vec::new() | |
- }; | |
- | |
- let lens = Self::lens_from_normal_lines( | |
- new_text.line_of_offset(new_text.len()) + 1, | |
- self.line_height, | |
- self.lens_height, | |
- &normal_lines, | |
- ); | |
- Syntax { | |
- rev: new_rev, | |
- language: self.language, | |
- tree: new_tree, | |
- text: new_text, | |
- lens, | |
- line_height: self.line_height, | |
- lens_height: self.lens_height, | |
- normal_lines, | |
- styles, | |
- } | |
- } | |
- | |
- pub fn update_lens_height(&mut self, line_height: usize, lens_height: usize) { | |
- self.lens = Self::lens_from_normal_lines( | |
- self.text.line_of_offset(self.text.len()) + 1, | |
- line_height, | |
- lens_height, | |
- &self.normal_lines, | |
- ); | |
- self.line_height = line_height; | |
- self.lens_height = lens_height; | |
- } | |
- | |
- pub fn lens_from_normal_lines( | |
- total_lines: usize, | |
- line_height: usize, | |
- lens_height: usize, | |
- normal_lines: &[usize], | |
- ) -> Lens { | |
- let mut builder = LensBuilder::new(); | |
- let mut current_line = 0; | |
- for normal_line in normal_lines.iter() { | |
- let normal_line = *normal_line; | |
- if normal_line > current_line { | |
- builder.add_section(normal_line - current_line, lens_height); | |
- } | |
- builder.add_section(1, line_height); | |
- current_line = normal_line + 1; | |
- } | |
- if current_line < total_lines { | |
- builder.add_section(total_lines - current_line, lens_height); | |
- } | |
- builder.build() | |
- } | |
- | |
- pub fn find_matching_pair(&self, offset: usize) -> Option<usize> { | |
- let tree = self.tree.as_ref()?; | |
- let node = tree | |
- .root_node() | |
- .descendant_for_byte_range(offset, offset + 1)?; | |
- let mut chars = node.kind().chars(); | |
- let char = chars.next()?; | |
- let char = matching_char(char)?; | |
- let tag = &char.to_string(); | |
- | |
- if let Some(offset) = self.find_tag_in_siblings(node, true, tag) { | |
- return Some(offset); | |
- } | |
- if let Some(offset) = self.find_tag_in_siblings(node, false, tag) { | |
- return Some(offset); | |
- } | |
- None | |
- } | |
- | |
- pub fn find_tag( | |
- &self, | |
- offset: usize, | |
- previous: bool, | |
- tag: &str, | |
- ) -> Option<usize> { | |
- let tree = self.tree.as_ref()?; | |
- let node = tree | |
- .root_node() | |
- .descendant_for_byte_range(offset, offset + 1)?; | |
- | |
- if let Some(offset) = self.find_tag_in_siblings(node, previous, tag) { | |
- return Some(offset); | |
- } | |
- | |
- if let Some(offset) = self.find_tag_in_children(node, tag) { | |
- return Some(offset); | |
- } | |
- | |
- let mut node = node; | |
- while let Some(parent) = node.parent() { | |
- if let Some(offset) = self.find_tag_in_siblings(parent, previous, tag) { | |
- return Some(offset); | |
- } | |
- node = parent; | |
- } | |
- None | |
- } | |
- | |
- fn find_tag_in_siblings( | |
- &self, | |
- node: Node, | |
- previous: bool, | |
- tag: &str, | |
- ) -> Option<usize> { | |
- let mut node = node; | |
- while let Some(sibling) = if previous { | |
- node.prev_sibling() | |
- } else { | |
- node.next_sibling() | |
- } { | |
- if sibling.kind() == tag { | |
- let offset = sibling.start_byte(); | |
- return Some(offset); | |
- } | |
- node = sibling; | |
- } | |
- None | |
- } | |
- | |
- fn find_tag_in_children(&self, node: Node, tag: &str) -> Option<usize> { | |
- for i in 0..node.child_count() { | |
- if let Some(child) = node.child(i) { | |
- if child.kind() == tag { | |
- let offset = child.start_byte(); | |
- return Some(offset); | |
- } | |
- } | |
- } | |
- None | |
- } | |
-} | |
- | |
-pub fn matching_pair_direction(c: char) -> Option<bool> { | |
- Some(match c { | |
- '{' => true, | |
- '}' => false, | |
- '(' => true, | |
- ')' => false, | |
- '[' => true, | |
- ']' => false, | |
- _ => return None, | |
- }) | |
-} | |
- | |
-pub fn matching_char(c: char) -> Option<char> { | |
- Some(match c { | |
- '{' => '}', | |
- '}' => '{', | |
- '(' => ')', | |
- ')' => '(', | |
- '[' => ']', | |
- ']' => '[', | |
- _ => return None, | |
- }) | |
-} | |
- | |
-pub fn has_unmatched_pair(line: &str) -> bool { | |
- let mut count = HashMap::new(); | |
- let mut pair_first = HashMap::new(); | |
- for c in line.chars().rev() { | |
- if let Some(left) = matching_pair_direction(c) { | |
- let key = if left { c } else { matching_char(c).unwrap() }; | |
- let pair_count = *count.get(&key).unwrap_or(&0i32); | |
- pair_first.entry(key).or_insert(left); | |
- if left { | |
- count.insert(key, pair_count - 1); | |
- } else { | |
- count.insert(key, pair_count + 1); | |
- } | |
- } | |
- } | |
- for (_, pair_count) in count.iter() { | |
- if *pair_count < 0 { | |
- return true; | |
- } | |
- } | |
- for (_, left) in pair_first.iter() { | |
- if *left { | |
- return true; | |
- } | |
- } | |
- false | |
-} | |
- | |
-pub fn str_is_pair_left(c: &str) -> bool { | |
- if c.chars().count() == 1 { | |
- let c = c.chars().next().unwrap(); | |
- if matching_pair_direction(c).unwrap_or(false) { | |
- return true; | |
- } | |
- } | |
- false | |
-} | |
- | |
-pub fn str_matching_pair(c: &str) -> Option<char> { | |
- if c.chars().count() == 1 { | |
- let c = c.chars().next().unwrap(); | |
- return matching_char(c); | |
- } | |
- None | |
-} | |
- | |
-#[cfg(test)] | |
-mod tests { | |
- use super::*; | |
- | |
- #[test] | |
- fn test_lens() { | |
- let lens = Syntax::lens_from_normal_lines(5, 25, 2, &[4]); | |
- assert_eq!(5, lens.len()); | |
- assert_eq!(8, lens.height_of_line(4)); | |
- assert_eq!(33, lens.height_of_line(5)); | |
- | |
- let lens = Syntax::lens_from_normal_lines(5, 25, 2, &[3]); | |
- assert_eq!(5, lens.len()); | |
- assert_eq!(6, lens.height_of_line(3)); | |
- assert_eq!(31, lens.height_of_line(4)); | |
- assert_eq!(33, lens.height_of_line(5)); | |
- } | |
- | |
- #[test] | |
- fn test_lens_iter() { | |
- let lens = Syntax::lens_from_normal_lines(5, 25, 2, &[0, 2, 4]); | |
- assert_eq!(5, lens.len()); | |
- let mut iter = lens.iter_chunks(2..5); | |
- assert_eq!(Some((2, 25)), iter.next()); | |
- assert_eq!(Some((3, 2)), iter.next()); | |
- assert_eq!(Some((4, 25)), iter.next()); | |
- assert_eq!(None, iter.next()); | |
- | |
- let lens = | |
- Syntax::lens_from_normal_lines(91, 25, 2, &[0, 11, 14, 54, 57, 90]); | |
- assert_eq!(91, lens.len()); | |
- let mut iter = lens.iter_chunks(89..91); | |
- assert_eq!(Some((89, 2)), iter.next()); | |
- assert_eq!(Some((90, 25)), iter.next()); | |
- assert_eq!(None, iter.next()); | |
- } | |
-} | |
diff --git a/lapce-core/src/syntax/edit.rs b/lapce-core/src/syntax/edit.rs | |
new file mode 100644 | |
index 000000000..4f86c2055 | |
--- /dev/null | |
+++ b/lapce-core/src/syntax/edit.rs | |
@@ -0,0 +1,102 @@ | |
+use tree_sitter::Point; | |
+use xi_rope::{multiset::CountMatcher, Rope, RopeDelta}; | |
+ | |
+use crate::buffer::InsertsValueIter; | |
+ | |
+fn point_at_offset(text: &Rope, offset: usize) -> Point { | |
+ let line = text.line_of_offset(offset); | |
+ let col = text.offset_of_line(line + 1) - offset; | |
+ Point::new(line, col) | |
+} | |
+ | |
+fn traverse(point: Point, text: &str) -> Point { | |
+ let Point { | |
+ mut row, | |
+ mut column, | |
+ } = point; | |
+ | |
+ for ch in text.chars() { | |
+ if ch == '\n' { | |
+ row += 1; | |
+ column = 0; | |
+ } else { | |
+ column += 1; | |
+ } | |
+ } | |
+ Point { row, column } | |
+} | |
+ | |
+fn create_insert_edit( | |
+ old_text: &Rope, | |
+ start: usize, | |
+ inserted: &Rope, | |
+) -> tree_sitter::InputEdit { | |
+ let start_position = point_at_offset(old_text, start); | |
+ tree_sitter::InputEdit { | |
+ start_byte: start, | |
+ old_end_byte: start, | |
+ new_end_byte: start + inserted.len(), | |
+ start_position, | |
+ old_end_position: start_position, | |
+ new_end_position: traverse( | |
+ start_position, | |
+ &inserted.slice_to_cow(0..inserted.len()), | |
+ ), | |
+ } | |
+} | |
+ | |
+fn create_delete_edit( | |
+ old_text: &Rope, | |
+ start: usize, | |
+ end: usize, | |
+) -> tree_sitter::InputEdit { | |
+ let start_position = point_at_offset(old_text, start); | |
+ let end_position = point_at_offset(old_text, end); | |
+ tree_sitter::InputEdit { | |
+ start_byte: start, | |
+ // The old end byte position was at the end | |
+ old_end_byte: end, | |
+ // but since we're deleting everything up to it, it gets 'moved' to where we start | |
+ new_end_byte: start, | |
+ | |
+ start_position, | |
+ old_end_position: end_position, | |
+ new_end_position: start_position, | |
+ } | |
+} | |
+ | |
+/// Add to a vector of treesitter edits from a delta | |
+pub fn generate_edits( | |
+ old_text: &Rope, | |
+ delta: &RopeDelta, | |
+ edits: &mut Vec<tree_sitter::InputEdit>, | |
+) { | |
+ let (interval, _) = delta.summary(); | |
+ let (start, end) = interval.start_end(); | |
+ if let Some(inserted) = delta.as_simple_insert() { | |
+ edits.push(create_insert_edit(old_text, start, inserted)); | |
+ } else if delta.is_simple_delete() { | |
+ edits.push(create_delete_edit(old_text, start, end)); | |
+ } else { | |
+ // TODO: This probably generates more insertions/deletions than it needs to. | |
+ // Which is why, for the common case of simple inserts/deletions, we use the above logic | |
+ | |
+ // Break the delta into two parts, the insertions and the deletions | |
+ // This makes it easier to translate them into the tree_sitter::InputEdit format | |
+ let (insertions, deletions) = delta.clone().factor(); | |
+ | |
+ for insert in InsertsValueIter::new(&insertions) { | |
+ // We may not need the inserted text in order to calculate the new end position | |
+ // but I was sufficiently uncertain, and so continued with how we did it previously | |
+ let start = insert.old_offset; | |
+ let inserted = insert.node; | |
+ edits.push(create_insert_edit(old_text, start, inserted)); | |
+ } | |
+ | |
+ // I believe this is the correct `CountMatcher` to use for this iteration, since it is what they use | |
+ // for deleting a subset from a string. | |
+ for (start, end) in deletions.range_iter(CountMatcher::Zero) { | |
+ edits.push(create_delete_edit(old_text, start, end)); | |
+ } | |
+ } | |
+} | |
diff --git a/lapce-core/src/syntax/highlight.rs b/lapce-core/src/syntax/highlight.rs | |
new file mode 100644 | |
index 000000000..afabcdbb7 | |
--- /dev/null | |
+++ b/lapce-core/src/syntax/highlight.rs | |
@@ -0,0 +1,836 @@ | |
+/* | |
+ * This Source Code Form is subject to the terms of the Mozilla Public | |
+ * License, v. 2.0. If a copy of the MPL was not distributed with this | |
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. | |
+ * | |
+ * Much of the code in this file is modified from [helix](https://github.com/helix-editor/helix)'s implementation of their syntax highlighting, which is under the MPL. | |
+ */ | |
+ | |
+use std::{ | |
+ borrow::Cow, | |
+ sync::{ | |
+ atomic::{AtomicUsize, Ordering}, | |
+ Arc, | |
+ }, | |
+}; | |
+ | |
+use arc_swap::ArcSwap; | |
+use tree_sitter::{ | |
+ Language, Point, Query, QueryCaptures, QueryCursor, QueryMatch, Tree, | |
+}; | |
+use xi_rope::Rope; | |
+ | |
+use crate::{language::LapceLanguage, style::SCOPES}; | |
+ | |
+use super::{util::RopeProvider, PARSER}; | |
+ | |
+macro_rules! declare_language_highlights { | |
+ ($($name:ident: $feature_name:expr),* $(,)?) => { | |
+ mod highlights { | |
+ // We allow non upper case globals to make the macro definition simpler. | |
+ #![allow(non_upper_case_globals)] | |
+ use once_cell::sync::Lazy; | |
+ use crate::language::LapceLanguage; | |
+ use std::sync::Arc; | |
+ use super::HighlightConfiguration; | |
+ | |
+ // We use Arcs because in the future we may want to load highlight configurations at runtime | |
+ $( | |
+ #[cfg(feature = $feature_name)] | |
+ pub static $name: Lazy<Arc<HighlightConfiguration>> = Lazy::new(|| Arc::new(LapceLanguage::$name.new_highlight_config())); | |
+ )* | |
+ } | |
+ | |
+ pub(crate) fn get_highlight_config(lang: LapceLanguage) -> Arc<HighlightConfiguration> { | |
+ match lang { | |
+ $( | |
+ #[cfg(feature = $feature_name)] | |
+ LapceLanguage::$name => highlights::$name.clone() | |
+ ),* | |
+ } | |
+ } | |
+ }; | |
+} | |
+ | |
+declare_language_highlights!( | |
+ Rust: "lang-rust", | |
+ Go: "lang-go", | |
+ Javascript: "lang-javascript", | |
+ Jsx: "lang-javascript", | |
+ Typescript: "lang-typescript", | |
+ Tsx: "lang-typescript", | |
+ Python: "lang-python", | |
+ Toml: "lang-toml", | |
+ Php: "lang-php", | |
+ Elixir: "lang-elixir", | |
+ C: "lang-c", | |
+ Cpp: "lang-cpp", | |
+ Json: "lang-json", | |
+ Markdown: "lang-markdown", | |
+ MarkdownInline: "lang-markdown", | |
+ Ruby: "lang-ruby", | |
+ Html: "lang-html", | |
+ Java: "lang-java", | |
+ Elm: "lang-elm", | |
+ Swift: "lang-swift", | |
+ Ql: "lang-ql", | |
+ Haskell: "lang-haskell", | |
+ Glimmer: "lang-glimmer", | |
+ Haxe: "lang-haxe", | |
+ Hcl: "lang-hcl", | |
+ Ocaml: "lang-ocaml", | |
+ OcamlInterface: "lang-ocaml", | |
+ Scss: "lang-scss", | |
+ Hare: "lang-hare", | |
+ Css: "lang-css", | |
+ Zig: "lang-zig", | |
+ Bash: "lang-bash", | |
+ Yaml: "lang-yaml", | |
+ Julia: "lang-julia", | |
+ Wgsl: "lang-wgsl", | |
+ Dockerfile: "lang-dockerfile", | |
+ Csharp: "lang-csharp", | |
+ Nix: "lang-nix", | |
+); | |
+ | |
+/// Indicates which highlight should be applied to a region of source code. | |
+#[derive(Copy, Clone, Debug, PartialEq, Eq)] | |
+pub struct Highlight(pub usize); | |
+ | |
+/// Represents a single step in rendering a syntax-highlighted document. | |
+#[derive(Copy, Clone, Debug)] | |
+pub enum HighlightEvent { | |
+ Source { start: usize, end: usize }, | |
+ HighlightStart(Highlight), | |
+ HighlightEnd, | |
+} | |
+ | |
+#[derive(Debug)] | |
+pub(crate) struct LocalDef<'a> { | |
+ name: Cow<'a, str>, | |
+ value_range: std::ops::Range<usize>, | |
+ highlight: Option<Highlight>, | |
+} | |
+ | |
+#[derive(Debug)] | |
+pub(crate) struct LocalScope<'a> { | |
+ pub(crate) inherits: bool, | |
+ pub(crate) range: std::ops::Range<usize>, | |
+ pub(crate) local_defs: Vec<LocalDef<'a>>, | |
+} | |
+ | |
+const CANCELLATION_CHECK_INTERVAL: usize = 100; | |
+ | |
+/// Contains the data needed to highlight code written in a particular language. | |
+/// | |
+/// This struct is immutable and can be shared between threads. | |
+#[derive(Debug)] | |
+pub struct HighlightConfiguration { | |
+ pub language: Language, | |
+ pub query: Query, | |
+ pub injections_query: Query, | |
+ pub combined_injections_query: Option<Query>, | |
+ pub highlights_pattern_index: usize, | |
+ pub highlight_indices: ArcSwap<Vec<Option<Highlight>>>, | |
+ pub non_local_variable_patterns: Vec<bool>, | |
+ pub injection_content_capture_index: Option<u32>, | |
+ pub injection_language_capture_index: Option<u32>, | |
+ pub local_scope_capture_index: Option<u32>, | |
+ pub local_def_capture_index: Option<u32>, | |
+ pub local_def_value_capture_index: Option<u32>, | |
+ pub local_ref_capture_index: Option<u32>, | |
+} | |
+ | |
+impl HighlightConfiguration { | |
+ /// Creates a `HighlightConfiguration` for a given `Language` and set of highlighting | |
+ /// queries. | |
+ /// | |
+ /// # Parameters | |
+ /// | |
+ /// * `language` - The Tree-sitter `Language` that should be used for parsing. | |
+ /// * `highlights_query` - A string containing tree patterns for syntax highlighting. This | |
+ /// should be non-empty, otherwise no syntax highlights will be added. | |
+ /// * `injections_query` - A string containing tree patterns for injecting other languages | |
+ /// into the document. This can be empty if no injections are desired. | |
+ /// * `locals_query` - A string containing tree patterns for tracking local variable | |
+ /// definitions and references. This can be empty if local variable tracking is not needed. | |
+ /// | |
+ /// Returns a `HighlightConfiguration` that can then be used with the `highlight` method. | |
+ pub fn new( | |
+ language: Language, | |
+ highlights_query: &str, | |
+ injection_query: &str, | |
+ locals_query: &str, | |
+ ) -> Result<Self, tree_sitter::QueryError> { | |
+ // Concatenate the query strings, keeping track of the start offset of each section. | |
+ let mut query_source = String::new(); | |
+ query_source.push_str(locals_query); | |
+ let highlights_query_offset = query_source.len(); | |
+ query_source.push_str(highlights_query); | |
+ | |
+ // Construct a single query by concatenating the three query strings, but record the | |
+ // range of pattern indices that belong to each individual string. | |
+ let query = Query::new(language, &query_source)?; | |
+ let mut highlights_pattern_index = 0; | |
+ for i in 0..(query.pattern_count()) { | |
+ let pattern_offset = query.start_byte_for_pattern(i); | |
+ if pattern_offset < highlights_query_offset { | |
+ highlights_pattern_index += 1; | |
+ } | |
+ } | |
+ | |
+ let mut injections_query = Query::new(language, injection_query)?; | |
+ | |
+ // Construct a separate query just for dealing with the 'combined injections'. | |
+ // Disable the combined injection patterns in the main query. | |
+ let mut combined_injections_query = Query::new(language, injection_query)?; | |
+ let mut has_combined_queries = false; | |
+ for pattern_index in 0..injections_query.pattern_count() { | |
+ let settings = injections_query.property_settings(pattern_index); | |
+ if settings.iter().any(|s| &*s.key == "injection.combined") { | |
+ has_combined_queries = true; | |
+ injections_query.disable_pattern(pattern_index); | |
+ } else { | |
+ combined_injections_query.disable_pattern(pattern_index); | |
+ } | |
+ } | |
+ let combined_injections_query = if has_combined_queries { | |
+ Some(combined_injections_query) | |
+ } else { | |
+ None | |
+ }; | |
+ | |
+ // Find all of the highlighting patterns that are disabled for nodes that | |
+ // have been identified as local variables. | |
+ let non_local_variable_patterns = (0..query.pattern_count()) | |
+ .map(|i| { | |
+ query.property_predicates(i).iter().any(|(prop, positive)| { | |
+ !*positive && prop.key.as_ref() == "local" | |
+ }) | |
+ }) | |
+ .collect(); | |
+ | |
+ // Store the numeric ids for all of the special captures. | |
+ let mut injection_content_capture_index = None; | |
+ let mut injection_language_capture_index = None; | |
+ let mut local_def_capture_index = None; | |
+ let mut local_def_value_capture_index = None; | |
+ let mut local_ref_capture_index = None; | |
+ let mut local_scope_capture_index = None; | |
+ for (i, name) in query.capture_names().iter().enumerate() { | |
+ let i = Some(i as u32); | |
+ match name.as_str() { | |
+ "local.definition" => local_def_capture_index = i, | |
+ "local.definition-value" => local_def_value_capture_index = i, | |
+ "local.reference" => local_ref_capture_index = i, | |
+ "local.scope" => local_scope_capture_index = i, | |
+ _ => {} | |
+ } | |
+ } | |
+ | |
+ for (i, name) in injections_query.capture_names().iter().enumerate() { | |
+ let i = Some(i as u32); | |
+ match name.as_str() { | |
+ "injection.content" => injection_content_capture_index = i, | |
+ "injection.language" => injection_language_capture_index = i, | |
+ _ => {} | |
+ } | |
+ } | |
+ | |
+ let highlight_indices: ArcSwap<Vec<_>> = | |
+ ArcSwap::from_pointee(vec![None; query.capture_names().len()]); | |
+ let conf = Self { | |
+ language, | |
+ query, | |
+ injections_query, | |
+ combined_injections_query, | |
+ highlights_pattern_index, | |
+ highlight_indices, | |
+ non_local_variable_patterns, | |
+ injection_content_capture_index, | |
+ injection_language_capture_index, | |
+ local_scope_capture_index, | |
+ local_def_capture_index, | |
+ local_def_value_capture_index, | |
+ local_ref_capture_index, | |
+ }; | |
+ conf.configure(SCOPES); | |
+ Ok(conf) | |
+ } | |
+ | |
+ /// Get a slice containing all of the highlight names used in the configuration. | |
+ pub fn names(&self) -> &[String] { | |
+ self.query.capture_names() | |
+ } | |
+ | |
+ /// Set the list of recognized highlight names. | |
+ /// | |
+ /// Tree-sitter syntax-highlighting queries specify highlights in the form of dot-separated | |
+ /// highlight names like `punctuation.bracket` and `function.method.builtin`. Consumers of | |
+ /// these queries can choose to recognize highlights with different levels of specificity. | |
+ /// For example, the string `function.builtin` will match against `function.builtin.constructor` | |
+ /// but will not match `function.method.builtin` and `function.method`. | |
+ /// | |
+ /// When highlighting, results are returned as `Highlight` values, which contain the index | |
+ /// of the matched highlight this list of highlight names. | |
+ pub fn configure(&self, recognized_names: &[&str]) { | |
+ let mut capture_parts = Vec::new(); | |
+ let indices: Vec<_> = self | |
+ .query | |
+ .capture_names() | |
+ .iter() | |
+ .map(move |capture_name| { | |
+ capture_parts.clear(); | |
+ capture_parts.extend(capture_name.split('.')); | |
+ | |
+ let mut best_index = None; | |
+ let mut best_match_len = 0; | |
+ for (i, recognized_name) in recognized_names.iter().enumerate() { | |
+ let recognized_name = recognized_name; | |
+ let mut len = 0; | |
+ let mut matches = true; | |
+ for (i, part) in recognized_name.split('.').enumerate() { | |
+ match capture_parts.get(i) { | |
+ Some(capture_part) if *capture_part == part => len += 1, | |
+ _ => { | |
+ matches = false; | |
+ break; | |
+ } | |
+ } | |
+ } | |
+ if matches && len > best_match_len { | |
+ best_index = Some(i); | |
+ best_match_len = len; | |
+ } | |
+ } | |
+ best_index.map(Highlight) | |
+ }) | |
+ .collect(); | |
+ | |
+ self.highlight_indices.store(Arc::new(indices)); | |
+ } | |
+} | |
+ | |
+#[derive(Debug)] | |
+pub(crate) struct HighlightIter<'a> { | |
+ pub(crate) source: &'a Rope, | |
+ pub(crate) byte_offset: usize, | |
+ pub(crate) cancellation_flag: Option<&'a AtomicUsize>, | |
+ pub(crate) layers: Vec<HighlightIterLayer<'a>>, | |
+ pub(crate) iter_count: usize, | |
+ pub(crate) next_event: Option<HighlightEvent>, | |
+ pub(crate) last_highlight_range: Option<(usize, usize, usize)>, | |
+} | |
+ | |
+pub(crate) struct HighlightIterLayer<'a> { | |
+ pub(crate) _tree: Option<Tree>, | |
+ pub(crate) cursor: QueryCursor, | |
+ pub(crate) captures: | |
+ std::iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>, | |
+ pub(crate) config: &'a HighlightConfiguration, | |
+ pub(crate) highlight_end_stack: Vec<usize>, | |
+ pub(crate) scope_stack: Vec<LocalScope<'a>>, | |
+ pub(crate) depth: usize, | |
+ pub(crate) ranges: &'a [tree_sitter::Range], | |
+} | |
+impl<'a> std::fmt::Debug for HighlightIterLayer<'a> { | |
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
+ f.debug_struct("HighlightIterLayer").finish() | |
+ } | |
+} | |
+ | |
+impl<'a> HighlightIterLayer<'a> { | |
+ // First, sort scope boundaries by their byte offset in the document. At a | |
+ // given position, emit scope endings before scope beginnings. Finally, emit | |
+ // scope boundaries from deeper layers first. | |
+ fn sort_key(&mut self) -> Option<(usize, bool, isize)> { | |
+ let depth = -(self.depth as isize); | |
+ let next_start = self | |
+ .captures | |
+ .peek() | |
+ .map(|(m, i)| m.captures[*i].node.start_byte()); | |
+ let next_end = self.highlight_end_stack.last().cloned(); | |
+ match (next_start, next_end) { | |
+ (Some(start), Some(end)) => { | |
+ if start < end { | |
+ Some((start, true, depth)) | |
+ } else { | |
+ Some((end, false, depth)) | |
+ } | |
+ } | |
+ (Some(i), None) => Some((i, true, depth)), | |
+ (None, Some(j)) => Some((j, false, depth)), | |
+ _ => None, | |
+ } | |
+ } | |
+} | |
+ | |
+impl<'a> HighlightIter<'a> { | |
+ fn emit_event( | |
+ &mut self, | |
+ offset: usize, | |
+ event: Option<HighlightEvent>, | |
+ ) -> Option<Result<HighlightEvent, super::Error>> { | |
+ let result; | |
+ if self.byte_offset < offset { | |
+ result = Some(Ok(HighlightEvent::Source { | |
+ start: self.byte_offset, | |
+ end: offset, | |
+ })); | |
+ self.byte_offset = offset; | |
+ self.next_event = event; | |
+ } else { | |
+ result = event.map(Ok); | |
+ } | |
+ self.sort_layers(); | |
+ result | |
+ } | |
+ | |
+ pub(crate) fn sort_layers(&mut self) { | |
+ while !self.layers.is_empty() { | |
+ if let Some(sort_key) = self.layers[0].sort_key() { | |
+ let mut i = 0; | |
+ while i + 1 < self.layers.len() { | |
+ if let Some(next_offset) = self.layers[i + 1].sort_key() { | |
+ if next_offset < sort_key { | |
+ i += 1; | |
+ continue; | |
+ } | |
+ } else { | |
+ let layer = self.layers.remove(i + 1); | |
+ PARSER.with(|ts_parser| { | |
+ let highlighter = &mut ts_parser.borrow_mut(); | |
+ highlighter.cursors.push(layer.cursor); | |
+ }); | |
+ } | |
+ break; | |
+ } | |
+ if i > 0 { | |
+ self.layers[0..(i + 1)].rotate_left(1); | |
+ } | |
+ break; | |
+ } else { | |
+ let layer = self.layers.remove(0); | |
+ PARSER.with(|ts_parser| { | |
+ let highlighter = &mut ts_parser.borrow_mut(); | |
+ highlighter.cursors.push(layer.cursor); | |
+ }); | |
+ } | |
+ } | |
+ } | |
+} | |
+ | |
+impl<'a> Iterator for HighlightIter<'a> { | |
+ type Item = Result<HighlightEvent, super::Error>; | |
+ | |
+ fn next(&mut self) -> Option<Self::Item> { | |
+ 'main: loop { | |
+ // If we've already determined the next highlight boundary, just return it. | |
+ if let Some(e) = self.next_event.take() { | |
+ return Some(Ok(e)); | |
+ } | |
+ | |
+ // Periodically check for cancellation, returning `Cancelled` error if the | |
+ // cancellation flag was flipped. | |
+ if let Some(cancellation_flag) = self.cancellation_flag { | |
+ self.iter_count += 1; | |
+ if self.iter_count >= CANCELLATION_CHECK_INTERVAL { | |
+ self.iter_count = 0; | |
+ if cancellation_flag.load(Ordering::Relaxed) != 0 { | |
+ return Some(Err(super::Error::Cancelled)); | |
+ } | |
+ } | |
+ } | |
+ | |
+ // If none of the layers have any more highlight boundaries, terminate. | |
+ if self.layers.is_empty() { | |
+ let len = self.source.len(); | |
+ return if self.byte_offset < len { | |
+ let result = Some(Ok(HighlightEvent::Source { | |
+ start: self.byte_offset, | |
+ end: len, | |
+ })); | |
+ self.byte_offset = len; | |
+ result | |
+ } else { | |
+ None | |
+ }; | |
+ } | |
+ | |
+ // Get the next capture from whichever layer has the earliest highlight boundary. | |
+ let range; | |
+ let layer = &mut self.layers[0]; | |
+ if let Some((next_match, capture_index)) = layer.captures.peek() { | |
+ let next_capture = next_match.captures[*capture_index]; | |
+ range = next_capture.node.byte_range(); | |
+ | |
+ // If any previous highlight ends before this node starts, then before | |
+ // processing this capture, emit the source code up until the end of the | |
+ // previous highlight, and an end event for that highlight. | |
+ if let Some(end_byte) = layer.highlight_end_stack.last().cloned() { | |
+ if end_byte <= range.start { | |
+ layer.highlight_end_stack.pop(); | |
+ return self.emit_event( | |
+ end_byte, | |
+ Some(HighlightEvent::HighlightEnd), | |
+ ); | |
+ } | |
+ } | |
+ } | |
+ // If there are no more captures, then emit any remaining highlight end events. | |
+ // And if there are none of those, then just advance to the end of the document. | |
+ else if let Some(end_byte) = layer.highlight_end_stack.last().cloned() | |
+ { | |
+ layer.highlight_end_stack.pop(); | |
+ return self | |
+ .emit_event(end_byte, Some(HighlightEvent::HighlightEnd)); | |
+ } else { | |
+ return self.emit_event(self.source.len(), None); | |
+ }; | |
+ | |
+ let (mut match_, capture_index) = layer.captures.next().unwrap(); | |
+ let mut capture = match_.captures[capture_index]; | |
+ | |
+ // Remove from the local scope stack any local scopes that have already ended. | |
+ while range.start > layer.scope_stack.last().unwrap().range.end { | |
+ layer.scope_stack.pop(); | |
+ } | |
+ | |
+ // If this capture is for tracking local variables, then process the | |
+ // local variable info. | |
+ let mut reference_highlight = None; | |
+ let mut definition_highlight = None; | |
+ while match_.pattern_index < layer.config.highlights_pattern_index { | |
+ // If the node represents a local scope, push a new local scope onto | |
+ // the scope stack. | |
+ if Some(capture.index) == layer.config.local_scope_capture_index { | |
+ definition_highlight = None; | |
+ let mut scope = LocalScope { | |
+ inherits: true, | |
+ range: range.clone(), | |
+ local_defs: Vec::new(), | |
+ }; | |
+ for prop in | |
+ layer.config.query.property_settings(match_.pattern_index) | |
+ { | |
+ if let "local.scope-inherits" = prop.key.as_ref() { | |
+ scope.inherits = prop | |
+ .value | |
+ .as_ref() | |
+ .map_or(true, |r| r.as_ref() == "true"); | |
+ } | |
+ } | |
+ layer.scope_stack.push(scope); | |
+ } | |
+ // If the node represents a definition, add a new definition to the | |
+ // local scope at the top of the scope stack. | |
+ else if Some(capture.index) == layer.config.local_def_capture_index | |
+ { | |
+ reference_highlight = None; | |
+ let scope = layer.scope_stack.last_mut().unwrap(); | |
+ | |
+ let mut value_range = 0..0; | |
+ for capture in match_.captures { | |
+ if Some(capture.index) | |
+ == layer.config.local_def_value_capture_index | |
+ { | |
+ value_range = capture.node.byte_range(); | |
+ } | |
+ } | |
+ | |
+ let name = self.source.slice_to_cow(range.clone()); | |
+ scope.local_defs.push(LocalDef { | |
+ name, | |
+ value_range, | |
+ highlight: None, | |
+ }); | |
+ definition_highlight = | |
+ scope.local_defs.last_mut().map(|s| &mut s.highlight); | |
+ } | |
+ // If the node represents a reference, then try to find the corresponding | |
+ // definition in the scope stack. | |
+ else if Some(capture.index) == layer.config.local_ref_capture_index | |
+ && definition_highlight.is_none() | |
+ { | |
+ definition_highlight = None; | |
+ let name = self.source.slice_to_cow(range.clone()); | |
+ for scope in layer.scope_stack.iter().rev() { | |
+ if let Some(highlight) = | |
+ scope.local_defs.iter().rev().find_map(|def| { | |
+ if def.name == name | |
+ && range.start >= def.value_range.end | |
+ { | |
+ Some(def.highlight) | |
+ } else { | |
+ None | |
+ } | |
+ }) | |
+ { | |
+ reference_highlight = highlight; | |
+ break; | |
+ } | |
+ if !scope.inherits { | |
+ break; | |
+ } | |
+ } | |
+ } | |
+ | |
+ // Continue processing any additional matches for the same node. | |
+ if let Some((next_match, next_capture_index)) = layer.captures.peek() | |
+ { | |
+ let next_capture = next_match.captures[*next_capture_index]; | |
+ if next_capture.node == capture.node { | |
+ capture = next_capture; | |
+ match_ = layer.captures.next().unwrap().0; | |
+ continue; | |
+ } | |
+ } | |
+ | |
+ self.sort_layers(); | |
+ continue 'main; | |
+ } | |
+ | |
+ // Otherwise, this capture must represent a highlight. | |
+ // If this exact range has already been highlighted by an earlier pattern, or by | |
+ // a different layer, then skip over this one. | |
+ if let Some((last_start, last_end, last_depth)) = | |
+ self.last_highlight_range | |
+ { | |
+ if range.start == last_start | |
+ && range.end == last_end | |
+ && layer.depth < last_depth | |
+ { | |
+ self.sort_layers(); | |
+ continue 'main; | |
+ } | |
+ } | |
+ | |
+ // If the current node was found to be a local variable, then skip over any | |
+ // highlighting patterns that are disabled for local variables. | |
+ if definition_highlight.is_some() || reference_highlight.is_some() { | |
+ while layer.config.non_local_variable_patterns[match_.pattern_index] | |
+ { | |
+ if let Some((next_match, next_capture_index)) = | |
+ layer.captures.peek() | |
+ { | |
+ let next_capture = next_match.captures[*next_capture_index]; | |
+ if next_capture.node == capture.node { | |
+ capture = next_capture; | |
+ match_ = layer.captures.next().unwrap().0; | |
+ continue; | |
+ } | |
+ } | |
+ | |
+ self.sort_layers(); | |
+ continue 'main; | |
+ } | |
+ } | |
+ | |
+ // Once a highlighting pattern is found for the current node, skip over | |
+ // any later highlighting patterns that also match this node. Captures | |
+ // for a given node are ordered by pattern index, so these subsequent | |
+ // captures are guaranteed to be for highlighting, not injections or | |
+ // local variables. | |
+ while let Some((next_match, next_capture_index)) = layer.captures.peek() | |
+ { | |
+ let next_capture = next_match.captures[*next_capture_index]; | |
+ if next_capture.node == capture.node { | |
+ layer.captures.next(); | |
+ } else { | |
+ break; | |
+ } | |
+ } | |
+ | |
+ let current_highlight = | |
+ layer.config.highlight_indices.load()[capture.index as usize]; | |
+ | |
+ // If this node represents a local definition, then store the current | |
+ // highlight value on the local scope entry representing this node. | |
+ if let Some(definition_highlight) = definition_highlight { | |
+ *definition_highlight = current_highlight; | |
+ } | |
+ | |
+ // Emit a scope start event and push the node's end position to the stack. | |
+ if let Some(highlight) = reference_highlight.or(current_highlight) { | |
+ self.last_highlight_range = | |
+ Some((range.start, range.end, layer.depth)); | |
+ layer.highlight_end_stack.push(range.end); | |
+ return self.emit_event( | |
+ range.start, | |
+ Some(HighlightEvent::HighlightStart(highlight)), | |
+ ); | |
+ } | |
+ | |
+ self.sort_layers(); | |
+ } | |
+ } | |
+} | |
+ | |
+#[derive(Clone)] | |
+pub(crate) enum IncludedChildren { | |
+ None, | |
+ All, | |
+ Unnamed, | |
+} | |
+ | |
+impl Default for IncludedChildren { | |
+ fn default() -> Self { | |
+ Self::None | |
+ } | |
+} | |
+ | |
+// Compute the ranges that should be included when parsing an injection. | |
+// This takes into account three things: | |
+// * `parent_ranges` - The ranges must all fall within the *current* layer's ranges. | |
+// * `nodes` - Every injection takes place within a set of nodes. The injection ranges | |
+// are the ranges of those nodes. | |
+// * `includes_children` - For some injections, the content nodes' children should be | |
+// excluded from the nested document, so that only the content nodes' *own* content | |
+// is reparsed. For other injections, the content nodes' entire ranges should be | |
+// reparsed, including the ranges of their children. | |
+pub(crate) fn intersect_ranges( | |
+ parent_ranges: &[tree_sitter::Range], | |
+ nodes: &[tree_sitter::Node], | |
+ included_children: IncludedChildren, | |
+) -> Vec<tree_sitter::Range> { | |
+ let mut cursor = nodes[0].walk(); | |
+ let mut result = Vec::new(); | |
+ let mut parent_range_iter = parent_ranges.iter(); | |
+ let mut parent_range = parent_range_iter | |
+ .next() | |
+ .expect("Layers should only be constructed with non-empty ranges vectors"); | |
+ for node in nodes.iter() { | |
+ let mut preceding_range = tree_sitter::Range { | |
+ start_byte: 0, | |
+ start_point: Point::new(0, 0), | |
+ end_byte: node.start_byte(), | |
+ end_point: node.start_position(), | |
+ }; | |
+ let following_range = tree_sitter::Range { | |
+ start_byte: node.end_byte(), | |
+ start_point: node.end_position(), | |
+ end_byte: usize::MAX, | |
+ end_point: Point::new(usize::MAX, usize::MAX), | |
+ }; | |
+ | |
+ for excluded_range in node | |
+ .children(&mut cursor) | |
+ .filter_map(|child| match included_children { | |
+ IncludedChildren::None => Some(child.range()), | |
+ IncludedChildren::All => None, | |
+ IncludedChildren::Unnamed => { | |
+ if child.is_named() { | |
+ Some(child.range()) | |
+ } else { | |
+ None | |
+ } | |
+ } | |
+ }) | |
+ .chain([following_range].iter().cloned()) | |
+ { | |
+ let mut range = tree_sitter::Range { | |
+ start_byte: preceding_range.end_byte, | |
+ start_point: preceding_range.end_point, | |
+ end_byte: excluded_range.start_byte, | |
+ end_point: excluded_range.start_point, | |
+ }; | |
+ preceding_range = excluded_range; | |
+ | |
+ if range.end_byte < parent_range.start_byte { | |
+ continue; | |
+ } | |
+ | |
+ while parent_range.start_byte <= range.end_byte { | |
+ if parent_range.end_byte > range.start_byte { | |
+ if range.start_byte < parent_range.start_byte { | |
+ range.start_byte = parent_range.start_byte; | |
+ range.start_point = parent_range.start_point; | |
+ } | |
+ | |
+ if parent_range.end_byte < range.end_byte { | |
+ if range.start_byte < parent_range.end_byte { | |
+ result.push(tree_sitter::Range { | |
+ start_byte: range.start_byte, | |
+ start_point: range.start_point, | |
+ end_byte: parent_range.end_byte, | |
+ end_point: parent_range.end_point, | |
+ }); | |
+ } | |
+ range.start_byte = parent_range.end_byte; | |
+ range.start_point = parent_range.end_point; | |
+ } else { | |
+ if range.start_byte < range.end_byte { | |
+ result.push(range); | |
+ } | |
+ break; | |
+ } | |
+ } | |
+ | |
+ if let Some(next_range) = parent_range_iter.next() { | |
+ parent_range = next_range; | |
+ } else { | |
+ return result; | |
+ } | |
+ } | |
+ } | |
+ } | |
+ result | |
+} | |
+ | |
+pub(crate) fn injection_for_match<'a>( | |
+ config: &HighlightConfiguration, | |
+ query: &'a Query, | |
+ query_match: &QueryMatch<'a, 'a>, | |
+ source: &'a Rope, | |
+) -> ( | |
+ Option<Cow<'a, str>>, | |
+ Option<tree_sitter::Node<'a>>, | |
+ IncludedChildren, | |
+) { | |
+ let content_capture_index = config.injection_content_capture_index; | |
+ let language_capture_index = config.injection_language_capture_index; | |
+ | |
+ let mut language_name = None; | |
+ let mut content_node = None; | |
+ for capture in query_match.captures { | |
+ let index = Some(capture.index); | |
+ if index == language_capture_index { | |
+ let name = source.slice_to_cow(capture.node.byte_range()); | |
+ language_name = Some(name); | |
+ } else if index == content_capture_index { | |
+ content_node = Some(capture.node); | |
+ } | |
+ } | |
+ | |
+ let mut included_children = IncludedChildren::default(); | |
+ for prop in query.property_settings(query_match.pattern_index) { | |
+ match prop.key.as_ref() { | |
+ // In addition to specifying the language name via the text of a | |
+ // captured node, it can also be hard-coded via a `#set!` predicate | |
+ // that sets the injection.language key. | |
+ "injection.language" => { | |
+ if language_name.is_none() { | |
+ language_name = prop.value.as_ref().map(|s| s.as_ref().into()) | |
+ } | |
+ } | |
+ | |
+ // By default, injections do not include the *children* of an | |
+ // `injection.content` node - only the ranges that belong to the | |
+ // node itself. This can be changed using a `#set!` predicate that | |
+ // sets the `injection.include-children` key. | |
+ "injection.include-children" => { | |
+ included_children = IncludedChildren::All | |
+ } | |
+ | |
+ // Some queries might only exclude named children but include unnamed | |
+ // children in their `injection.content` node. This can be enabled using | |
+ // a `#set!` predicate that sets the `injection.include-unnamed-children` key. | |
+ "injection.include-unnamed-children" => { | |
+ included_children = IncludedChildren::Unnamed | |
+ } | |
+ _ => {} | |
+ } | |
+ } | |
+ | |
+ (language_name, content_node, included_children) | |
+} | |
diff --git a/lapce-core/src/syntax/mod.rs b/lapce-core/src/syntax/mod.rs | |
new file mode 100644 | |
index 000000000..ec1b17b5f | |
--- /dev/null | |
+++ b/lapce-core/src/syntax/mod.rs | |
@@ -0,0 +1,807 @@ | |
+/* | |
+ * This Source Code Form is subject to the terms of the Mozilla Public | |
+ * License, v. 2.0. If a copy of the MPL was not distributed with this | |
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. | |
+ * | |
+ * Much of the code in this file is modified from [helix](https://github.com/helix-editor/helix)'s implementation of their syntax highlighting, which is under the MPL. | |
+ */ | |
+ | |
+use std::{ | |
+ cell::RefCell, | |
+ collections::{HashSet, VecDeque}, | |
+ mem, | |
+ path::Path, | |
+ sync::{atomic::AtomicUsize, Arc}, | |
+}; | |
+ | |
+use itertools::Itertools; | |
+use lapce_rpc::style::Style; | |
+use slotmap::{DefaultKey as LayerId, HopSlotMap}; | |
+use thiserror::Error; | |
+use tree_sitter::{Node, Parser, Point, QueryCursor, Tree}; | |
+use xi_rope::{ | |
+ spans::{Spans, SpansBuilder}, | |
+ Interval, Rope, RopeDelta, | |
+}; | |
+ | |
+use crate::{ | |
+ language::LapceLanguage, | |
+ lens::{Lens, LensBuilder}, | |
+ style::SCOPES, | |
+}; | |
+ | |
+use self::{ | |
+ edit::generate_edits, | |
+ highlight::{ | |
+ get_highlight_config, injection_for_match, intersect_ranges, Highlight, | |
+ HighlightConfiguration, HighlightEvent, HighlightIter, HighlightIterLayer, | |
+ IncludedChildren, LocalScope, | |
+ }, | |
+ util::{matching_char, RopeProvider}, | |
+}; | |
+ | |
+mod edit; | |
+pub mod highlight; | |
+pub mod util; | |
+ | |
+// Uses significant portions Helix's implementation, and on tree-sitter's highlighter implementation | |
+ | |
+pub struct TsParser { | |
+ parser: tree_sitter::Parser, | |
+ pub cursors: Vec<QueryCursor>, | |
+} | |
+ | |
+thread_local! { | |
+ pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser { | |
+ parser: Parser::new(), | |
+ cursors: Vec::new(), | |
+ }); | |
+} | |
+ | |
+/// Represents the reason why syntax highlighting failed. | |
+#[derive(Debug, Error, PartialEq, Eq)] | |
+pub enum Error { | |
+ #[error("Cancelled")] | |
+ Cancelled, | |
+ #[error("Invalid language")] | |
+ InvalidLanguage, | |
+ #[error("Unknown error")] | |
+ Unknown, | |
+} | |
+ | |
+#[derive(Debug, Clone)] | |
+pub struct LanguageLayer { | |
+ // mode | |
+ // grammar | |
+ pub config: Arc<HighlightConfiguration>, | |
+ pub(crate) tree: Option<Tree>, | |
+ pub ranges: Vec<tree_sitter::Range>, | |
+ pub depth: usize, | |
+} | |
+ | |
+impl LanguageLayer { | |
+ pub fn tree(&self) -> &Tree { | |
+ self.tree.as_ref().unwrap() | |
+ } | |
+ | |
+ pub fn try_tree(&self) -> Option<&Tree> { | |
+ self.tree.as_ref() | |
+ } | |
+ | |
+ fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> { | |
+ parser.set_included_ranges(&self.ranges).unwrap(); | |
+ | |
+ parser | |
+ .set_language(self.config.language) | |
+ .map_err(|_| Error::InvalidLanguage)?; | |
+ | |
+ // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) }; | |
+ let tree = parser | |
+ .parse_with( | |
+ &mut |byte, _| { | |
+ if byte <= source.len() { | |
+ source | |
+ .iter_chunks(byte..) | |
+ .next() | |
+ .map(|s| s.as_bytes()) | |
+ .unwrap_or(&[]) | |
+ } else { | |
+ &[] | |
+ } | |
+ }, | |
+ self.tree.as_ref(), | |
+ ) | |
+ .ok_or(Error::Cancelled)?; | |
+ // unsafe { ts_parser.parser.set_cancellation_flag(None) }; | |
+ self.tree = Some(tree); | |
+ Ok(()) | |
+ } | |
+} | |
+ | |
+#[derive(Clone)] | |
+pub struct SyntaxLayers { | |
+ layers: HopSlotMap<LayerId, LanguageLayer>, | |
+ root: LayerId, | |
+} | |
+impl SyntaxLayers { | |
+ pub fn new_empty(config: Arc<HighlightConfiguration>) -> SyntaxLayers { | |
+ Self::new(None, config) | |
+ } | |
+ | |
+ pub fn new( | |
+ source: Option<&Rope>, | |
+ config: Arc<HighlightConfiguration>, | |
+ ) -> SyntaxLayers { | |
+ let root_layer = LanguageLayer { | |
+ tree: None, | |
+ config, | |
+ depth: 0, | |
+ ranges: vec![tree_sitter::Range { | |
+ start_byte: 0, | |
+ end_byte: usize::MAX, | |
+ start_point: Point::new(0, 0), | |
+ end_point: Point::new(usize::MAX, usize::MAX), | |
+ }], | |
+ }; | |
+ | |
+ let mut layers = HopSlotMap::default(); | |
+ let root = layers.insert(root_layer); | |
+ | |
+ let mut syntax = SyntaxLayers { root, layers }; | |
+ | |
+ if let Some(source) = source { | |
+ let _ = syntax.update(source, source, None); | |
+ } | |
+ | |
+ syntax | |
+ } | |
+ | |
+ pub fn update( | |
+ &mut self, | |
+ old_source: &Rope, | |
+ source: &Rope, | |
+ deltas: Option<&[RopeDelta]>, | |
+ ) -> Result<(), Error> { | |
+ let mut queue = VecDeque::new(); | |
+ queue.push_back(self.root); | |
+ | |
+ let injection_callback = |language: &str| { | |
+ LapceLanguage::from_name(language.to_string()).map(get_highlight_config) | |
+ }; | |
+ | |
+ let mut edits = Vec::new(); | |
+ if let Some(deltas) = deltas { | |
+ let mut current_source = old_source.clone(); | |
+ for delta in deltas { | |
+ generate_edits(¤t_source, delta, &mut edits); | |
+ // Apply the edit, since later deltas can rely on this one having occurred | |
+ current_source = delta.apply(¤t_source); | |
+ } | |
+ } | |
+ | |
+ // Use the edits to update all layers markers | |
+ if !edits.is_empty() { | |
+ fn point_add(a: Point, b: Point) -> Point { | |
+ if b.row > 0 { | |
+ Point::new(a.row.saturating_add(b.row), b.column) | |
+ } else { | |
+ Point::new(0, a.column.saturating_add(b.column)) | |
+ } | |
+ } | |
+ fn point_sub(a: Point, b: Point) -> Point { | |
+ if a.row > b.row { | |
+ Point::new(a.row.saturating_sub(b.row), a.column) | |
+ } else { | |
+ Point::new(0, a.column.saturating_sub(b.column)) | |
+ } | |
+ } | |
+ | |
+ for layer in &mut self.layers.values_mut() { | |
+ // The root layer always covers the whole range (0..usize::MAX) | |
+ if layer.depth == 0 { | |
+ continue; | |
+ } | |
+ | |
+ for range in &mut layer.ranges { | |
+ // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720 | |
+ for edit in edits.iter().rev() { | |
+ let is_pure_insertion = edit.old_end_byte == edit.start_byte; | |
+ | |
+ // if edit is after range, skip | |
+ if edit.start_byte > range.end_byte { | |
+ // TODO: || (is_noop && edit.start_byte == range.end_byte) | |
+ continue; | |
+ } | |
+ | |
+ // if edit is before range, shift entire range by len | |
+ if edit.old_end_byte < range.start_byte { | |
+ range.start_byte = edit.new_end_byte | |
+ + (range.start_byte - edit.old_end_byte); | |
+ range.start_point = point_add( | |
+ edit.new_end_position, | |
+ point_sub(range.start_point, edit.old_end_position), | |
+ ); | |
+ | |
+ range.end_byte = edit | |
+ .new_end_byte | |
+ .saturating_add(range.end_byte - edit.old_end_byte); | |
+ range.end_point = point_add( | |
+ edit.new_end_position, | |
+ point_sub(range.end_point, edit.old_end_position), | |
+ ); | |
+ } | |
+ // if the edit starts in the space before and extends into the range | |
+ else if edit.start_byte < range.start_byte { | |
+ range.start_byte = edit.new_end_byte; | |
+ range.start_point = edit.new_end_position; | |
+ | |
+ range.end_byte = range | |
+ .end_byte | |
+ .saturating_sub(edit.old_end_byte) | |
+ .saturating_add(edit.new_end_byte); | |
+ range.end_point = point_add( | |
+ edit.new_end_position, | |
+ point_sub(range.end_point, edit.old_end_position), | |
+ ); | |
+ } | |
+ // If the edit is an insertion at the start of the tree, shift | |
+ else if edit.start_byte == range.start_byte | |
+ && is_pure_insertion | |
+ { | |
+ range.start_byte = edit.new_end_byte; | |
+ range.start_point = edit.new_end_position; | |
+ } else { | |
+ range.end_byte = range | |
+ .end_byte | |
+ .saturating_sub(edit.old_end_byte) | |
+ .saturating_add(edit.new_end_byte); | |
+ range.end_point = point_add( | |
+ edit.new_end_position, | |
+ point_sub(range.end_point, edit.old_end_position), | |
+ ); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ PARSER.with(|ts_parser| { | |
+ let ts_parser = &mut ts_parser.borrow_mut(); | |
+ let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new); | |
+ // TODO: might need to set cursor range | |
+ cursor.set_byte_range(0..usize::MAX); | |
+ | |
+ let mut touched = HashSet::new(); | |
+ | |
+ // TODO: we should be able to avoid editing & parsing layers with ranges earlier in the document before the edit | |
+ | |
+ while let Some(layer_id) = queue.pop_front() { | |
+ // Mark the layer as touched | |
+ touched.insert(layer_id); | |
+ | |
+ let layer = &mut self.layers[layer_id]; | |
+ | |
+ // If a tree already exists, notify it of changes. | |
+ if let Some(tree) = &mut layer.tree { | |
+ for edit in edits.iter().rev() { | |
+ // Apply the edits in reverse. | |
+ // If we applied them in order then edit 1 would disrupt the positioning of edit 2. | |
+ tree.edit(edit); | |
+ } | |
+ } | |
+ | |
+ // Re-parse the tree. | |
+ layer.parse(&mut ts_parser.parser, source)?; | |
+ | |
+ // Switch to an immutable borrow. | |
+ let layer = &self.layers[layer_id]; | |
+ | |
+ // Process injections. | |
+ let matches = cursor.matches( | |
+ &layer.config.injections_query, | |
+ layer.tree().root_node(), | |
+ RopeProvider(source), | |
+ ); | |
+ let mut injections = Vec::new(); | |
+ for mat in matches { | |
+ let (language_name, content_node, included_children) = injection_for_match( | |
+ &layer.config, | |
+ &layer.config.injections_query, | |
+ &mat, | |
+ source, | |
+ ); | |
+ | |
+ // Explicitly remove this match so that none of its other captures will remain | |
+ // in the stream of captures. | |
+ mat.remove(); | |
+ | |
+ // If a language is found with the given name, then add a new language layer | |
+ // to the highlighted document. | |
+ if let (Some(language_name), Some(content_node)) = (language_name, content_node) | |
+ { | |
+ if let Some(config) = (injection_callback)(&language_name) { | |
+ let ranges = | |
+ intersect_ranges(&layer.ranges, &[content_node], included_children); | |
+ | |
+ if !ranges.is_empty() { | |
+ injections.push((config, ranges)); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ // Process combined injections. | |
+ if let Some(combined_injections_query) = &layer.config.combined_injections_query { | |
+ let mut injections_by_pattern_index = | |
+ vec![ | |
+ (None, Vec::new(), IncludedChildren::default()); | |
+ combined_injections_query.pattern_count() | |
+ ]; | |
+ let matches = cursor.matches( | |
+ combined_injections_query, | |
+ layer.tree().root_node(), | |
+ RopeProvider(source), | |
+ ); | |
+ for mat in matches { | |
+ let entry = &mut injections_by_pattern_index[mat.pattern_index]; | |
+ let (language_name, content_node, included_children) = injection_for_match( | |
+ &layer.config, | |
+ combined_injections_query, | |
+ &mat, | |
+ source, | |
+ ); | |
+ if language_name.is_some() { | |
+ entry.0 = language_name; | |
+ } | |
+ if let Some(content_node) = content_node { | |
+ entry.1.push(content_node); | |
+ } | |
+ entry.2 = included_children; | |
+ } | |
+ for (lang_name, content_nodes, included_children) in injections_by_pattern_index | |
+ { | |
+ if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) { | |
+ if let Some(config) = (injection_callback)(&lang_name) { | |
+ let ranges = intersect_ranges( | |
+ &layer.ranges, | |
+ &content_nodes, | |
+ included_children, | |
+ ); | |
+ if !ranges.is_empty() { | |
+ injections.push((config, ranges)); | |
+ } | |
+ } | |
+ } | |
+ } | |
+ } | |
+ | |
+ let depth = layer.depth + 1; | |
+ // TODO: can't inline this since matches borrows self.layers | |
+ for (config, ranges) in injections { | |
+ // Find an existing layer | |
+ let layer = self | |
+ .layers | |
+ .iter_mut() | |
+ .find(|(_, layer)| { | |
+ layer.depth == depth && // TODO: track parent id instead | |
+ layer.config.language == config.language && layer.ranges == ranges | |
+ }) | |
+ .map(|(id, _layer)| id); | |
+ | |
+ // ...or insert a new one. | |
+ let layer_id = layer.unwrap_or_else(|| { | |
+ self.layers.insert(LanguageLayer { | |
+ tree: None, | |
+ config, | |
+ depth, | |
+ ranges, | |
+ }) | |
+ }); | |
+ | |
+ queue.push_back(layer_id); | |
+ } | |
+ | |
+ // TODO: pre-process local scopes at this time, rather than highlight? | |
+ // would solve problems with locals not working across boundaries | |
+ } | |
+ | |
+ // Return the cursor back in the pool. | |
+ ts_parser.cursors.push(cursor); | |
+ | |
+ // Remove all untouched layers | |
+ self.layers.retain(|id, _| touched.contains(&id)); | |
+ | |
+ Ok(()) | |
+ }) | |
+ } | |
+ | |
+ pub fn tree(&self) -> &Tree { | |
+ self.layers[self.root].tree() | |
+ } | |
+ | |
+ pub fn try_tree(&self) -> Option<&Tree> { | |
+ self.layers[self.root].try_tree() | |
+ } | |
+ | |
+ /// Iterate over the highlighted regions for a given slice of source code. | |
+ pub fn highlight_iter<'a>( | |
+ &'a self, | |
+ source: &'a Rope, | |
+ range: Option<std::ops::Range<usize>>, | |
+ cancellation_flag: Option<&'a AtomicUsize>, | |
+ ) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a { | |
+ let mut layers = self | |
+ .layers | |
+ .iter() | |
+ .filter_map(|(_, layer)| { | |
+ // TODO: if range doesn't overlap layer range, skip it | |
+ | |
+ // Reuse a cursor from the pool if available. | |
+ let mut cursor = PARSER.with(|ts_parser| { | |
+ let highlighter = &mut ts_parser.borrow_mut(); | |
+ highlighter.cursors.pop().unwrap_or_else(QueryCursor::new) | |
+ }); | |
+ | |
+ // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which | |
+ // prevents them from being moved. But both of these values are really just | |
+ // pointers, so it's actually ok to move them. | |
+ let cursor_ref = unsafe { | |
+ mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) | |
+ }; | |
+ | |
+ // if reusing cursors & no range this resets to whole range | |
+ cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); | |
+ | |
+ let mut captures = cursor_ref | |
+ .captures( | |
+ &layer.config.query, | |
+ layer.tree().root_node(), | |
+ RopeProvider(source), | |
+ ) | |
+ .peekable(); | |
+ | |
+ // If there's no captures, skip the layer | |
+ captures.peek()?; | |
+ | |
+ Some(HighlightIterLayer { | |
+ highlight_end_stack: Vec::new(), | |
+ scope_stack: vec![LocalScope { | |
+ inherits: false, | |
+ range: 0..usize::MAX, | |
+ local_defs: Vec::new(), | |
+ }], | |
+ cursor, | |
+ _tree: None, | |
+ captures, | |
+ config: layer.config.as_ref(), // TODO: just reuse `layer` | |
+ depth: layer.depth, // TODO: just reuse `layer` | |
+ ranges: &layer.ranges, // TODO: temp | |
+ }) | |
+ }) | |
+ .collect::<Vec<_>>(); | |
+ | |
+ // HAXX: arrange layers by byte range, with deeper layers positioned first | |
+ layers.sort_by_key(|layer| { | |
+ ( | |
+ layer.ranges.first().cloned(), | |
+ std::cmp::Reverse(layer.depth), | |
+ ) | |
+ }); | |
+ | |
+ let mut result = HighlightIter { | |
+ source, | |
+ byte_offset: range.map_or(0, |r| r.start), | |
+ cancellation_flag, | |
+ iter_count: 0, | |
+ layers, | |
+ next_event: None, | |
+ last_highlight_range: None, | |
+ }; | |
+ result.sort_layers(); | |
+ result | |
+ } | |
+ | |
+ // Commenting | |
+ // comment_strings_for_pos | |
+ // is_commented | |
+ | |
+ // Indentation | |
+ // suggested_indent_for_line_at_buffer_row | |
+ // suggested_indent_for_buffer_row | |
+ // indent_level_for_line | |
+ | |
+ // TODO: Folding | |
+} | |
+ | |
+#[derive(Clone)] | |
+pub struct Syntax { | |
+ pub rev: u64, | |
+ pub language: LapceLanguage, | |
+ pub text: Rope, | |
+ pub layers: SyntaxLayers, | |
+ pub lens: Lens, | |
+ pub normal_lines: Vec<usize>, | |
+ pub line_height: usize, | |
+ pub lens_height: usize, | |
+ pub styles: Option<Arc<Spans<Style>>>, | |
+} | |
+ | |
+impl std::fmt::Debug for Syntax { | |
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
+ f.debug_struct("Syntax") | |
+ .field("rev", &self.rev) | |
+ .field("language", &self.language) | |
+ .field("text", &self.text) | |
+ .field("normal_lines", &self.normal_lines) | |
+ .field("line_height", &self.line_height) | |
+ .field("lens_height", &self.lens_height) | |
+ .field("styles", &self.styles) | |
+ .finish() | |
+ } | |
+} | |
+ | |
+impl Syntax { | |
+ pub fn init(path: &Path) -> Option<Syntax> { | |
+ LapceLanguage::from_path(path).map(Syntax::from_language) | |
+ } | |
+ | |
+ pub fn from_language(language: LapceLanguage) -> Syntax { | |
+ Syntax { | |
+ rev: 0, | |
+ language, | |
+ text: Rope::from(""), | |
+ layers: SyntaxLayers::new_empty(get_highlight_config(language)), | |
+ lens: Self::lens_from_normal_lines(0, 0, 0, &Vec::new()), | |
+ line_height: 0, | |
+ lens_height: 0, | |
+ normal_lines: Vec::new(), | |
+ styles: None, | |
+ } | |
+ } | |
+ | |
+ pub fn parse( | |
+ &mut self, | |
+ new_rev: u64, | |
+ new_text: Rope, | |
+ deltas: Option<&[RopeDelta]>, | |
+ ) { | |
+ // Update it if the revision is new, or if it is doing the initial update | |
+ let change_count = deltas.map(|deltas| deltas.len()).unwrap_or(0).max(1); | |
+ let tree = if new_rev == self.rev + change_count as u64 | |
+ || (self.rev == 0 && new_rev == 0) | |
+ { | |
+ if self.layers.update(&self.text, &new_text, deltas).is_err() { | |
+ // We failed to update. However, we still want to update the rev and the like | |
+ // TODO: It would be good to try reparsing the entire file, in case it was an issue with the deltas | |
+ log::warn!("Failed to update syntax highlighting!"); | |
+ None | |
+ } else { | |
+ self.layers.try_tree() | |
+ } | |
+ } else { | |
+ self.layers.try_tree() | |
+ }; | |
+ | |
+ let styles = if tree.is_some() { | |
+ let mut current_hl: Option<Highlight> = None; | |
+ let mut highlights: SpansBuilder<Style> = | |
+ SpansBuilder::new(new_text.len()); | |
+ | |
+ // TODO: Should we be ignoring highlight errors via flattening them? | |
+ for highlight in self | |
+ .layers | |
+ .highlight_iter(&new_text, Some(0..new_text.len()), None) | |
+ .flatten() | |
+ { | |
+ match highlight { | |
+ HighlightEvent::Source { start, end } => { | |
+ if let Some(hl) = current_hl { | |
+ if let Some(hl) = SCOPES.get(hl.0) { | |
+ highlights.add_span( | |
+ Interval::new(start, end), | |
+ Style { | |
+ fg_color: Some(hl.to_string()), | |
+ }, | |
+ ); | |
+ } | |
+ } | |
+ } | |
+ HighlightEvent::HighlightStart(hl) => { | |
+ current_hl = Some(hl); | |
+ } | |
+ HighlightEvent::HighlightEnd => current_hl = None, | |
+ } | |
+ } | |
+ | |
+ Some(Arc::new(highlights.build())) | |
+ } else { | |
+ None | |
+ }; | |
+ | |
+ let normal_lines = if let Some(tree) = tree { | |
+ let mut cursor = tree.walk(); | |
+ let mut normal_lines = HashSet::new(); | |
+ self.language.walk_tree(&mut cursor, &mut normal_lines); | |
+ normal_lines.into_iter().sorted().collect::<Vec<usize>>() | |
+ } else { | |
+ Vec::new() | |
+ }; | |
+ | |
+ let lens = Self::lens_from_normal_lines( | |
+ new_text.line_of_offset(new_text.len()) + 1, | |
+ self.line_height, | |
+ self.lens_height, | |
+ &normal_lines, | |
+ ); | |
+ | |
+ self.rev = new_rev; | |
+ self.lens = lens; | |
+ self.normal_lines = normal_lines; | |
+ self.styles = styles; | |
+ self.text = new_text | |
+ | |
+ // Syntax { | |
+ // rev: new_rev, | |
+ // language: self.language, | |
+ // text: new_text, | |
+ // layers, | |
+ // lens, | |
+ // line_height: self.line_height, | |
+ // lens_height: self.lens_height, | |
+ // normal_lines, | |
+ // styles, | |
+ // } | |
+ } | |
+ | |
+ pub fn update_lens_height(&mut self, line_height: usize, lens_height: usize) { | |
+ self.lens = Self::lens_from_normal_lines( | |
+ self.text.line_of_offset(self.text.len()) + 1, | |
+ line_height, | |
+ lens_height, | |
+ &self.normal_lines, | |
+ ); | |
+ self.line_height = line_height; | |
+ self.lens_height = lens_height; | |
+ } | |
+ | |
+ pub fn lens_from_normal_lines( | |
+ total_lines: usize, | |
+ line_height: usize, | |
+ lens_height: usize, | |
+ normal_lines: &[usize], | |
+ ) -> Lens { | |
+ let mut builder = LensBuilder::new(); | |
+ let mut current_line = 0; | |
+ for normal_line in normal_lines.iter() { | |
+ let normal_line = *normal_line; | |
+ if normal_line > current_line { | |
+ builder.add_section(normal_line - current_line, lens_height); | |
+ } | |
+ builder.add_section(1, line_height); | |
+ current_line = normal_line + 1; | |
+ } | |
+ if current_line < total_lines { | |
+ builder.add_section(total_lines - current_line, lens_height); | |
+ } | |
+ builder.build() | |
+ } | |
+ | |
+ pub fn find_matching_pair(&self, offset: usize) -> Option<usize> { | |
+ let tree = self.layers.try_tree()?; | |
+ let node = tree | |
+ .root_node() | |
+ .descendant_for_byte_range(offset, offset + 1)?; | |
+ let mut chars = node.kind().chars(); | |
+ let char = chars.next()?; | |
+ let char = matching_char(char)?; | |
+ let tag = &char.to_string(); | |
+ | |
+ if let Some(offset) = self.find_tag_in_siblings(node, true, tag) { | |
+ return Some(offset); | |
+ } | |
+ if let Some(offset) = self.find_tag_in_siblings(node, false, tag) { | |
+ return Some(offset); | |
+ } | |
+ None | |
+ } | |
+ | |
+ pub fn find_tag( | |
+ &self, | |
+ offset: usize, | |
+ previous: bool, | |
+ tag: &str, | |
+ ) -> Option<usize> { | |
+ let tree = self.layers.try_tree()?; | |
+ let node = tree | |
+ .root_node() | |
+ .descendant_for_byte_range(offset, offset + 1)?; | |
+ | |
+ if let Some(offset) = self.find_tag_in_siblings(node, previous, tag) { | |
+ return Some(offset); | |
+ } | |
+ | |
+ if let Some(offset) = self.find_tag_in_children(node, tag) { | |
+ return Some(offset); | |
+ } | |
+ | |
+ let mut node = node; | |
+ while let Some(parent) = node.parent() { | |
+ if let Some(offset) = self.find_tag_in_siblings(parent, previous, tag) { | |
+ return Some(offset); | |
+ } | |
+ node = parent; | |
+ } | |
+ None | |
+ } | |
+ | |
+ fn find_tag_in_siblings( | |
+ &self, | |
+ node: Node, | |
+ previous: bool, | |
+ tag: &str, | |
+ ) -> Option<usize> { | |
+ let mut node = node; | |
+ while let Some(sibling) = if previous { | |
+ node.prev_sibling() | |
+ } else { | |
+ node.next_sibling() | |
+ } { | |
+ if sibling.kind() == tag { | |
+ let offset = sibling.start_byte(); | |
+ return Some(offset); | |
+ } | |
+ node = sibling; | |
+ } | |
+ None | |
+ } | |
+ | |
+ fn find_tag_in_children(&self, node: Node, tag: &str) -> Option<usize> { | |
+ for i in 0..node.child_count() { | |
+ if let Some(child) = node.child(i) { | |
+ if child.kind() == tag { | |
+ let offset = child.start_byte(); | |
+ return Some(offset); | |
+ } | |
+ } | |
+ } | |
+ None | |
+ } | |
+} | |
+ | |
+#[cfg(test)] | |
+mod tests { | |
+ use super::*; | |
+ | |
+ #[test] | |
+ fn test_lens() { | |
+ let lens = Syntax::lens_from_normal_lines(5, 25, 2, &[4]); | |
+ assert_eq!(5, lens.len()); | |
+ assert_eq!(8, lens.height_of_line(4)); | |
+ assert_eq!(33, lens.height_of_line(5)); | |
+ | |
+ let lens = Syntax::lens_from_normal_lines(5, 25, 2, &[3]); | |
+ assert_eq!(5, lens.len()); | |
+ assert_eq!(6, lens.height_of_line(3)); | |
+ assert_eq!(31, lens.height_of_line(4)); | |
+ assert_eq!(33, lens.height_of_line(5)); | |
+ } | |
+ | |
+ #[test] | |
+ fn test_lens_iter() { | |
+ let lens = Syntax::lens_from_normal_lines(5, 25, 2, &[0, 2, 4]); | |
+ assert_eq!(5, lens.len()); | |
+ let mut iter = lens.iter_chunks(2..5); | |
+ assert_eq!(Some((2, 25)), iter.next()); | |
+ assert_eq!(Some((3, 2)), iter.next()); | |
+ assert_eq!(Some((4, 25)), iter.next()); | |
+ assert_eq!(None, iter.next()); | |
+ | |
+ let lens = | |
+ Syntax::lens_from_normal_lines(91, 25, 2, &[0, 11, 14, 54, 57, 90]); | |
+ assert_eq!(91, lens.len()); | |
+ let mut iter = lens.iter_chunks(89..91); | |
+ assert_eq!(Some((89, 2)), iter.next()); | |
+ assert_eq!(Some((90, 25)), iter.next()); | |
+ assert_eq!(None, iter.next()); | |
+ } | |
+} | |
diff --git a/lapce-core/src/syntax/util.rs b/lapce-core/src/syntax/util.rs | |
new file mode 100644 | |
index 000000000..b56702142 | |
--- /dev/null | |
+++ b/lapce-core/src/syntax/util.rs | |
@@ -0,0 +1,97 @@ | |
+use std::collections::HashMap; | |
+ | |
+use tree_sitter::TextProvider; | |
+use xi_rope::{rope::ChunkIter, Rope}; | |
+ | |
+pub struct RopeChunksIterBytes<'a> { | |
+ chunks: ChunkIter<'a>, | |
+} | |
+impl<'a> Iterator for RopeChunksIterBytes<'a> { | |
+ type Item = &'a [u8]; | |
+ fn next(&mut self) -> Option<Self::Item> { | |
+ self.chunks.next().map(str::as_bytes) | |
+ } | |
+} | |
+ | |
+/// This allows tree-sitter to iterate over our Rope without us having to convert it into | |
+/// a contiguous byte-list. | |
+pub struct RopeProvider<'a>(pub &'a Rope); | |
+impl<'a> TextProvider<'a> for RopeProvider<'a> { | |
+ type I = RopeChunksIterBytes<'a>; | |
+ fn text(&mut self, node: tree_sitter::Node) -> Self::I { | |
+ let start = node.start_byte(); | |
+ let end = node.end_byte().min(self.0.len()); | |
+ let chunks = self.0.iter_chunks(start..end); | |
+ RopeChunksIterBytes { chunks } | |
+ } | |
+} | |
+ | |
+pub fn matching_pair_direction(c: char) -> Option<bool> { | |
+ Some(match c { | |
+ '{' => true, | |
+ '}' => false, | |
+ '(' => true, | |
+ ')' => false, | |
+ '[' => true, | |
+ ']' => false, | |
+ _ => return None, | |
+ }) | |
+} | |
+ | |
+pub fn matching_char(c: char) -> Option<char> { | |
+ Some(match c { | |
+ '{' => '}', | |
+ '}' => '{', | |
+ '(' => ')', | |
+ ')' => '(', | |
+ '[' => ']', | |
+ ']' => '[', | |
+ _ => return None, | |
+ }) | |
+} | |
+ | |
+pub fn has_unmatched_pair(line: &str) -> bool { | |
+ let mut count = HashMap::new(); | |
+ let mut pair_first = HashMap::new(); | |
+ for c in line.chars().rev() { | |
+ if let Some(left) = matching_pair_direction(c) { | |
+ let key = if left { c } else { matching_char(c).unwrap() }; | |
+ let pair_count = *count.get(&key).unwrap_or(&0i32); | |
+ pair_first.entry(key).or_insert(left); | |
+ if left { | |
+ count.insert(key, pair_count - 1); | |
+ } else { | |
+ count.insert(key, pair_count + 1); | |
+ } | |
+ } | |
+ } | |
+ for (_, pair_count) in count.iter() { | |
+ if *pair_count < 0 { | |
+ return true; | |
+ } | |
+ } | |
+ for (_, left) in pair_first.iter() { | |
+ if *left { | |
+ return true; | |
+ } | |
+ } | |
+ false | |
+} | |
+ | |
+pub fn str_is_pair_left(c: &str) -> bool { | |
+ if c.chars().count() == 1 { | |
+ let c = c.chars().next().unwrap(); | |
+ if matching_pair_direction(c).unwrap_or(false) { | |
+ return true; | |
+ } | |
+ } | |
+ false | |
+} | |
+ | |
+pub fn str_matching_pair(c: &str) -> Option<char> { | |
+ if c.chars().count() == 1 { | |
+ let c = c.chars().next().unwrap(); | |
+ return matching_char(c); | |
+ } | |
+ None | |
+} | |
diff --git a/lapce-core/src/word.rs b/lapce-core/src/word.rs | |
index 2308e715d..6d20401d7 100644 | |
--- a/lapce-core/src/word.rs | |
+++ b/lapce-core/src/word.rs | |
@@ -1,6 +1,6 @@ | |
use xi_rope::{Cursor, Rope, RopeInfo}; | |
-use crate::syntax::{matching_char, matching_pair_direction}; | |
+use crate::syntax::util::{matching_char, matching_pair_direction}; | |
#[derive(Copy, Clone, PartialEq, Eq)] | |
pub enum WordProperty { | |
diff --git a/lapce-data/src/config.rs b/lapce-data/src/config.rs | |
index d3dc5c8de..f4fae89f7 100644 | |
--- a/lapce-data/src/config.rs | |
+++ b/lapce-data/src/config.rs | |
@@ -221,7 +221,11 @@ pub struct EditorConfig { | |
impl EditorConfig { | |
pub fn font_family(&self) -> FontFamily { | |
- FontFamily::new_unchecked(self.font_family.clone()) | |
+ if self.font_family.is_empty() { | |
+ FontFamily::SYSTEM_UI | |
+ } else { | |
+ FontFamily::new_unchecked(self.font_family.clone()) | |
+ } | |
} | |
pub fn inlay_hint_font_family(&self) -> FontFamily { | |
@@ -263,11 +267,11 @@ impl EditorConfig { | |
#[serde(rename_all = "kebab-case")] | |
pub struct UIConfig { | |
#[field_names( | |
- desc = "Set the ui font family. If empty, it uses system default." | |
+ desc = "Set the UI font family. If empty, it uses system default." | |
)] | |
font_family: String, | |
- #[field_names(desc = "Set the ui base font size")] | |
+ #[field_names(desc = "Set the UI base font size")] | |
font_size: usize, | |
#[field_names( | |
@@ -303,7 +307,7 @@ impl UIConfig { | |
if self.font_family.is_empty() { | |
FontFamily::SYSTEM_UI | |
} else { | |
- FontFamily::new_unchecked(self.font_family.clone()) | |
+ FontFamily::new_unchecked(self.font_family.as_str()) | |
} | |
} | |
@@ -341,7 +345,7 @@ impl UIConfig { | |
if self.hover_font_family.is_empty() { | |
self.font_family() | |
} else { | |
- FontFamily::new_unchecked(self.hover_font_family.clone()) | |
+ FontFamily::new_unchecked(self.hover_font_family.as_str()) | |
} | |
} | |
diff --git a/lapce-data/src/data.rs b/lapce-data/src/data.rs | |
index ff4a6bdf7..69c2c8f20 100644 | |
--- a/lapce-data/src/data.rs | |
+++ b/lapce-data/src/data.rs | |
@@ -2,7 +2,7 @@ use std::{ | |
cell::RefCell, | |
cmp::Ordering, | |
collections::{HashMap, HashSet}, | |
- io::BufReader, | |
+ io::{BufReader, Read, Write}, | |
path::{Path, PathBuf}, | |
rc::Rc, | |
sync::Arc, | |
@@ -25,7 +25,6 @@ use lapce_core::{ | |
command::{FocusCommand, MultiSelectionCommand}, | |
cursor::{Cursor, CursorMode}, | |
editor::EditType, | |
- language::LapceLanguage, | |
mode::MotionMode, | |
movement::Movement, | |
register::Register, | |
@@ -298,11 +297,11 @@ impl LapceData { | |
let socket = | |
interprocess::local_socket::LocalSocketListener::bind(local_socket)?; | |
- for stream in socket.incoming().flatten() { | |
- let mut reader = BufReader::new(stream); | |
+ for mut stream in socket.incoming().flatten() { | |
let event_sink = event_sink.clone(); | |
thread::spawn(move || -> Result<()> { | |
loop { | |
+ let mut reader = BufReader::new(stream); | |
let msg: RpcMessage< | |
CoreRequest, | |
CoreNotification, | |
@@ -331,6 +330,10 @@ impl LapceData { | |
Target::Global, | |
); | |
} | |
+ | |
+ stream = reader.into_inner(); | |
+ let _ = stream.write_all(b"received"); | |
+ let _ = stream.flush(); | |
} | |
}); | |
} | |
@@ -353,6 +356,23 @@ impl LapceData { | |
files, | |
}); | |
lapce_rpc::stdio::write_msg(&mut socket, msg)?; | |
+ | |
+ let (tx, rx) = crossbeam_channel::bounded(1); | |
+ thread::spawn(move || { | |
+ let mut buf = [0; 100]; | |
+ let received = if let Ok(n) = socket.read(&mut buf) { | |
+ &buf[..n] == b"received" | |
+ } else { | |
+ false | |
+ }; | |
+ tx.send(received) | |
+ }); | |
+ | |
+ let received = rx.recv_timeout(std::time::Duration::from_millis(500))?; | |
+ if !received { | |
+ return Err(anyhow!("didn't receive response")); | |
+ } | |
+ | |
Ok(()) | |
} | |
} | |
@@ -368,9 +388,10 @@ impl LapceData { | |
/// normally only one window tab), size, position etc. and `Arc` references to | |
/// state that is common to this instance of Lapce, such as configuration and the | |
/// keymap setup. | |
-#[derive(Clone)] | |
+#[derive(Clone, Data)] | |
pub struct LapceWindowData { | |
/// The unique identifier for the Window. Generated by Druid. | |
+ #[data(ignore)] | |
pub window_id: WindowId, | |
/// The set of tabs within the window. These tabs are high-level | |
/// constructs, in particular they are not **editor tabs**, which are | |
@@ -386,7 +407,7 @@ pub struct LapceWindowData { | |
/// The index of the active window tab. | |
pub active: usize, | |
/// The Id of the active window tab. | |
- pub active_id: WidgetId, | |
+ pub active_id: Arc<WidgetId>, | |
pub keypress: Arc<KeyPressData>, | |
pub config: Arc<Config>, | |
pub db: Arc<LapceDb>, | |
@@ -400,19 +421,6 @@ pub struct LapceWindowData { | |
pub latest_release: Arc<Option<ReleaseInfo>>, | |
} | |
-impl Data for LapceWindowData { | |
- fn same(&self, other: &Self) -> bool { | |
- self.active == other.active | |
- && self.tabs.same(&other.tabs) | |
- && self.size.same(&other.size) | |
- && self.pos.same(&other.pos) | |
- && self.maximised.same(&other.maximised) | |
- && self.keypress.same(&other.keypress) | |
- && self.panel_orders.same(&other.panel_orders) | |
- && self.latest_release.same(&other.latest_release) | |
- } | |
-} | |
- | |
impl LapceWindowData { | |
pub fn new( | |
keypress: Arc<KeyPressData>, | |
@@ -499,7 +507,7 @@ impl LapceWindowData { | |
tabs, | |
tabs_order: Arc::new(tabs_order), | |
active, | |
- active_id: active_tab_id, | |
+ active_id: Arc::new(active_tab_id), | |
keypress, | |
config, | |
db, | |
@@ -520,7 +528,7 @@ impl LapceWindowData { | |
.enumerate() | |
.map(|(i, w)| { | |
let tab = self.tabs.get(w).unwrap(); | |
- if tab.id == self.active_id { | |
+ if tab.id == *self.active_id { | |
active_tab = i; | |
} | |
(*tab.workspace).clone() | |
@@ -568,10 +576,11 @@ pub enum DragContent { | |
Panel(PanelKind, Rect), | |
} | |
-#[derive(Clone, Lens)] | |
+#[derive(Clone, Lens, Data)] | |
pub struct LapceTabData { | |
+ #[data(ignore)] | |
pub id: WidgetId, | |
- pub window_id: WindowId, | |
+ pub window_id: Arc<WindowId>, | |
pub multiple_tab: bool, | |
pub workspace: Arc<LapceWorkspace>, | |
pub main_split: LapceMainSplitData, | |
@@ -587,56 +596,30 @@ pub struct LapceTabData { | |
pub plugin: Arc<PluginData>, | |
pub picker: Arc<FilePickerData>, | |
pub file_explorer: Arc<FileExplorerData>, | |
+ #[data(ignore)] | |
pub proxy: Arc<LapceProxy>, | |
pub proxy_status: Arc<ProxyStatus>, | |
pub keypress: Arc<KeyPressData>, | |
pub settings: Arc<LapceSettingsPanelData>, | |
pub about: Arc<AboutData>, | |
pub alert: Arc<AlertData>, | |
+ #[data(ignore)] | |
pub term_tx: Arc<Sender<(TermId, TermEvent)>>, | |
+ #[data(ignore)] | |
pub term_rx: Option<Receiver<(TermId, TermEvent)>>, | |
+ #[data(ignore)] | |
pub window_origin: Rc<RefCell<Point>>, | |
pub panel: Arc<PanelData>, | |
pub config: Arc<Config>, | |
- pub focus: WidgetId, | |
+ pub focus: Arc<WidgetId>, | |
pub focus_area: FocusArea, | |
+ #[data(ignore)] | |
pub db: Arc<LapceDb>, | |
- pub progresses: im::Vector<WorkProgress>, | |
+ pub progresses: Arc<Vec<WorkProgress>>, | |
pub drag: Arc<Option<(Vec2, Vec2, DragContent)>>, | |
pub latest_release: Arc<Option<ReleaseInfo>>, | |
} | |
-impl Data for LapceTabData { | |
- fn same(&self, other: &Self) -> bool { | |
- self.main_split.same(&other.main_split) | |
- && self.completion.same(&other.completion) | |
- && self.hover.same(&other.hover) | |
- && self.rename.same(&other.rename) | |
- && self.palette.same(&other.palette) | |
- && self.workspace.same(&other.workspace) | |
- && self.source_control.same(&other.source_control) | |
- && self.panel.same(&other.panel) | |
- && self.config.same(&other.config) | |
- && self.terminal.same(&other.terminal) | |
- && self.focus == other.focus | |
- && self.focus_area == other.focus_area | |
- && self.proxy_status.same(&other.proxy_status) | |
- && self.find.same(&other.find) | |
- && self.about.same(&other.about) | |
- && self.alert.same(&other.alert) | |
- && self.progresses.ptr_eq(&other.progresses) | |
- && self.file_explorer.same(&other.file_explorer) | |
- && self.plugin.same(&other.plugin) | |
- && self.problem.same(&other.problem) | |
- && self.search.same(&other.search) | |
- && self.picker.same(&other.picker) | |
- && self.drag.same(&other.drag) | |
- && self.keypress.same(&other.keypress) | |
- && self.settings.same(&other.settings) | |
- && self.latest_release.same(&other.latest_release) | |
- } | |
-} | |
- | |
impl GetConfig for LapceTabData { | |
fn get_config(&self) -> &Config { | |
&self.config | |
@@ -787,9 +770,9 @@ impl LapceTabData { | |
let mut tab = Self { | |
id: tab_id, | |
multiple_tab: false, | |
- window_id, | |
+ window_id: Arc::new(window_id), | |
workspace: Arc::new(workspace), | |
- focus, | |
+ focus: Arc::new(focus), | |
main_split, | |
completion, | |
hover, | |
@@ -816,7 +799,7 @@ impl LapceTabData { | |
config, | |
focus_area: FocusArea::Editor, | |
db, | |
- progresses: im::Vector::new(), | |
+ progresses: Arc::new(Vec::new()), | |
drag: Arc::new(None), | |
latest_release, | |
}; | |
@@ -1265,7 +1248,7 @@ impl LapceTabData { | |
} | |
} | |
LapceWorkbenchCommand::OpenSettings => { | |
- self.main_split.open_settings(ctx, false); | |
+ self.main_split.open_settings(ctx, false, &self.config); | |
} | |
LapceWorkbenchCommand::OpenSettingsFile => { | |
if let Some(path) = Config::settings_file() { | |
@@ -1306,7 +1289,7 @@ impl LapceTabData { | |
} | |
} | |
LapceWorkbenchCommand::OpenKeyboardShortcuts => { | |
- self.main_split.open_settings(ctx, true); | |
+ self.main_split.open_settings(ctx, true, &self.config); | |
} | |
LapceWorkbenchCommand::OpenKeyboardShortcutsFile => { | |
if let Some(path) = Config::keymaps_file() { | |
@@ -1397,14 +1380,14 @@ impl LapceTabData { | |
LapceWorkbenchCommand::NewWindow => { | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
- LapceUICommand::NewWindow(self.window_id), | |
+ LapceUICommand::NewWindow(*self.window_id), | |
Target::Global, | |
)); | |
} | |
LapceWorkbenchCommand::CloseWindow => { | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
- LapceUICommand::CloseWindow(self.window_id), | |
+ LapceUICommand::CloseWindow(*self.window_id), | |
Target::Auto, | |
)); | |
} | |
@@ -1699,8 +1682,8 @@ impl LapceTabData { | |
); | |
} | |
CommandKind::Focus(_) | CommandKind::Edit(_) | CommandKind::Move(_) => { | |
- let widget_id = if self.focus != self.palette.input_editor { | |
- self.focus | |
+ let widget_id = if *self.focus != self.palette.input_editor { | |
+ *self.focus | |
} else if let Some(active_tab) = self.main_split.active_tab.as_ref() | |
{ | |
self.main_split | |
@@ -1710,7 +1693,7 @@ impl LapceTabData { | |
.active_child() | |
.widget_id() | |
} else { | |
- self.focus | |
+ *self.focus | |
}; | |
ctx.submit_command(Command::new( | |
@@ -2217,7 +2200,7 @@ impl LapceMainSplitData { | |
None | |
} | |
}) | |
- .sorted_by_key(|(path, _)| (*path).clone()) | |
+ .sorted_by_key(|(path, _)| *path) | |
.collect() | |
} | |
@@ -2337,10 +2320,24 @@ impl LapceMainSplitData { | |
&mut self, | |
_ctx: &mut EventCtx, | |
editor_tab_id: WidgetId, | |
+ config: &Config, | |
) -> WidgetId { | |
let editor_tab = self.editor_tabs.get_mut(&editor_tab_id).unwrap(); | |
let editor_tab = Arc::make_mut(editor_tab); | |
- let child = EditorTabChild::Settings(WidgetId::next(), editor_tab_id); | |
+ let editor = LapceEditorData::new( | |
+ None, | |
+ None, | |
+ None, | |
+ BufferContent::Local(LocalBufferKind::Keymap), | |
+ config, | |
+ ); | |
+ let keymap_input_view_id = editor.view_id; | |
+ self.editors.insert(editor.view_id, Arc::new(editor)); | |
+ let child = EditorTabChild::Settings { | |
+ settings_widget_id: WidgetId::next(), | |
+ editor_tab_id, | |
+ keymap_input_view_id, | |
+ }; | |
editor_tab.children.push(child.clone()); | |
child.widget_id() | |
} | |
@@ -2545,14 +2542,19 @@ impl LapceMainSplitData { | |
} | |
} | |
- pub fn open_settings(&mut self, ctx: &mut EventCtx, show_key_bindings: bool) { | |
+ pub fn open_settings( | |
+ &mut self, | |
+ ctx: &mut EventCtx, | |
+ show_key_bindings: bool, | |
+ config: &Config, | |
+ ) { | |
let widget_id = match *self.active_tab { | |
Some(active) => { | |
let editor_tab = | |
Arc::make_mut(self.editor_tabs.get_mut(&active).unwrap()); | |
let mut existing: Option<WidgetId> = None; | |
for (i, child) in editor_tab.children.iter().enumerate() { | |
- if let EditorTabChild::Settings(_, _) = child { | |
+ if let EditorTabChild::Settings { .. } = child { | |
editor_tab.active = i; | |
existing = Some(child.widget_id()); | |
break; | |
@@ -2562,10 +2564,20 @@ impl LapceMainSplitData { | |
if let Some(widget_id) = existing { | |
widget_id | |
} else { | |
- let child = EditorTabChild::Settings( | |
- WidgetId::next(), | |
- editor_tab.widget_id, | |
+ let editor = LapceEditorData::new( | |
+ None, | |
+ None, | |
+ None, | |
+ BufferContent::Local(LocalBufferKind::Keymap), | |
+ config, | |
); | |
+ let keymap_input_view_id = editor.view_id; | |
+ self.editors.insert(editor.view_id, Arc::new(editor)); | |
+ let child = EditorTabChild::Settings { | |
+ settings_widget_id: WidgetId::next(), | |
+ editor_tab_id: editor_tab.widget_id, | |
+ keymap_input_view_id, | |
+ }; | |
editor_tab | |
.children | |
.insert(editor_tab.active + 1, child.clone()); | |
@@ -2583,7 +2595,7 @@ impl LapceMainSplitData { | |
} | |
None => { | |
let editor_tab_id = self.new_editor_tab(ctx, *self.split_id); | |
- self.editor_tab_new_settings(ctx, editor_tab_id) | |
+ self.editor_tab_new_settings(ctx, editor_tab_id, config) | |
} | |
}; | |
ctx.submit_command(Command::new( | |
@@ -2671,12 +2683,29 @@ impl LapceMainSplitData { | |
editor_view_id | |
} | |
+ pub fn can_jump_location_backward(&self) -> bool { | |
+ self.current_location >= 1 | |
+ } | |
+ | |
+ pub fn can_jump_location_forward(&self) -> bool { | |
+ !(self.locations.is_empty() | |
+ || self.current_location >= self.locations.len() - 1) | |
+ } | |
+ | |
pub fn save_jump_location( | |
&mut self, | |
path: PathBuf, | |
offset: usize, | |
scroll_offset: Vec2, | |
) { | |
+ if let Some(last_location) = self.locations.last() { | |
+ if last_location.path == path | |
+ && last_location.position == Some(offset) | |
+ && last_location.scroll_offset == Some(scroll_offset) | |
+ { | |
+ return; | |
+ } | |
+ } | |
let location = EditorLocation { | |
path, | |
position: Some(offset), | |
@@ -2727,7 +2756,7 @@ impl LapceMainSplitData { | |
} | |
} | |
} | |
- EditorTabChild::Settings(_, _) => {} | |
+ EditorTabChild::Settings { .. } => {} | |
} | |
} | |
@@ -2735,7 +2764,10 @@ impl LapceMainSplitData { | |
let id = self.new_file(ctx, config); | |
let doc = self.scratch_docs.get_mut(&id).unwrap(); | |
let doc = Arc::make_mut(doc); | |
- doc.set_language(LapceLanguage::Toml); | |
+ | |
+ #[cfg(feature = "lang-toml")] | |
+ doc.set_language(lapce_core::language::LapceLanguage::Toml); | |
+ | |
doc.reload(Rope::from(config.export_theme()), true); | |
} | |
@@ -3497,19 +3529,31 @@ impl LapceMainSplitData { | |
ctx: &mut EventCtx, | |
editor_tab_id: WidgetId, | |
direction: SplitDirection, | |
+ config: &Config, | |
) { | |
let editor_tab = self.editor_tabs.get(&editor_tab_id).unwrap(); | |
let split_id = editor_tab.split; | |
+ let editor = LapceEditorData::new( | |
+ None, | |
+ None, | |
+ None, | |
+ BufferContent::Local(LocalBufferKind::Keymap), | |
+ config, | |
+ ); | |
+ let keymap_input_view_id = editor.view_id; | |
+ self.editors.insert(editor.view_id, Arc::new(editor)); | |
+ | |
let new_editor_tab_id = WidgetId::next(); | |
let mut new_editor_tab = LapceEditorTabData { | |
widget_id: new_editor_tab_id, | |
split: split_id, | |
active: 0, | |
- children: vec![EditorTabChild::Settings( | |
- WidgetId::next(), | |
- new_editor_tab_id, | |
- )], | |
+ children: vec![EditorTabChild::Settings { | |
+ settings_widget_id: WidgetId::next(), | |
+ editor_tab_id: new_editor_tab_id, | |
+ keymap_input_view_id, | |
+ }], | |
layout_rect: Rc::new(RefCell::new(Rect::ZERO)), | |
content_is_hot: Rc::new(RefCell::new(false)), | |
}; | |
@@ -3598,14 +3642,20 @@ pub enum InlineFindDirection { | |
#[derive(Clone, Debug, PartialEq, Eq)] | |
pub enum EditorTabChild { | |
Editor(WidgetId, WidgetId, Option<(WidgetId, WidgetId)>), | |
- Settings(WidgetId, WidgetId), | |
+ Settings { | |
+ settings_widget_id: WidgetId, | |
+ editor_tab_id: WidgetId, | |
+ keymap_input_view_id: WidgetId, | |
+ }, | |
} | |
impl EditorTabChild { | |
pub fn widget_id(&self) -> WidgetId { | |
match &self { | |
EditorTabChild::Editor(widget_id, _, _) => *widget_id, | |
- EditorTabChild::Settings(widget_id, _) => *widget_id, | |
+ EditorTabChild::Settings { | |
+ settings_widget_id, .. | |
+ } => *settings_widget_id, | |
} | |
} | |
@@ -3615,23 +3665,23 @@ impl EditorTabChild { | |
let editor_data = data.main_split.editors.get(view_id).unwrap(); | |
EditorTabChildInfo::Editor(editor_data.editor_info(data)) | |
} | |
- EditorTabChild::Settings(_, _) => EditorTabChildInfo::Settings, | |
+ EditorTabChild::Settings { .. } => EditorTabChildInfo::Settings, | |
} | |
} | |
pub fn set_editor_tab( | |
&mut self, | |
data: &mut LapceTabData, | |
- editor_tab_id: WidgetId, | |
+ editor_tab_widget_id: WidgetId, | |
) { | |
match self { | |
EditorTabChild::Editor(view_id, _, _) => { | |
let editor_data = data.main_split.editors.get_mut(view_id).unwrap(); | |
let editor_data = Arc::make_mut(editor_data); | |
- editor_data.tab_id = Some(editor_tab_id); | |
+ editor_data.tab_id = Some(editor_tab_widget_id); | |
} | |
- EditorTabChild::Settings(_, current_editor_tab_id) => { | |
- *current_editor_tab_id = editor_tab_id; | |
+ EditorTabChild::Settings { editor_tab_id, .. } => { | |
+ *editor_tab_id = editor_tab_widget_id; | |
} | |
} | |
} | |
diff --git a/lapce-data/src/db.rs b/lapce-data/src/db.rs | |
index 5c717fe15..d76a02218 100644 | |
--- a/lapce-data/src/db.rs | |
+++ b/lapce-data/src/db.rs | |
@@ -21,7 +21,7 @@ use crate::{ | |
LapceMainSplitData, LapceTabData, LapceWindowData, LapceWorkspace, | |
SplitContent, SplitData, | |
}, | |
- document::{BufferContent, Document}, | |
+ document::{BufferContent, Document, LocalBufferKind}, | |
editor::EditorLocation, | |
panel::{PanelData, PanelOrder}, | |
split::SplitDirection, | |
@@ -166,7 +166,21 @@ impl EditorTabChildInfo { | |
) | |
} | |
EditorTabChildInfo::Settings => { | |
- EditorTabChild::Settings(WidgetId::next(), editor_tab_id) | |
+ let editor = LapceEditorData::new( | |
+ None, | |
+ None, | |
+ None, | |
+ BufferContent::Local(LocalBufferKind::Keymap), | |
+ config, | |
+ ); | |
+ let keymap_input_view_id = editor.view_id; | |
+ data.editors.insert(editor.view_id, Arc::new(editor)); | |
+ | |
+ EditorTabChild::Settings { | |
+ settings_widget_id: WidgetId::next(), | |
+ editor_tab_id, | |
+ keymap_input_view_id, | |
+ } | |
} | |
} | |
} | |
@@ -661,7 +675,7 @@ impl LapceDb { | |
.enumerate() | |
.map(|(i, w)| { | |
let tab = data.tabs.get(w).unwrap(); | |
- if tab.id == data.active_id { | |
+ if tab.id == *data.active_id { | |
active_tab = i; | |
} | |
(*tab.workspace).clone() | |
diff --git a/lapce-data/src/document.rs b/lapce-data/src/document.rs | |
index c208e4573..7bbc1072b 100644 | |
--- a/lapce-data/src/document.rs | |
+++ b/lapce-data/src/document.rs | |
@@ -4,17 +4,14 @@ use std::{ | |
collections::{HashMap, HashSet}, | |
path::{Path, PathBuf}, | |
rc::Rc, | |
- sync::{ | |
- atomic::{self}, | |
- Arc, | |
- }, | |
+ sync::Arc, | |
}; | |
use druid::{ | |
piet::{ | |
PietText, PietTextLayout, Text, TextAttribute, TextLayout, TextLayoutBuilder, | |
}, | |
- Color, ExtEventSink, Point, SingleUse, Size, Target, Vec2, WidgetId, | |
+ Color, ExtEventSink, Point, Size, Target, Vec2, WidgetId, | |
}; | |
use lapce_core::{ | |
buffer::{Buffer, DiffLines, InvalLines}, | |
@@ -849,13 +846,13 @@ impl Document { | |
} | |
} | |
- fn on_update(&mut self, delta: Option<&RopeDelta>) { | |
+ fn on_update(&mut self, deltas: Option<SmallVec<[RopeDelta; 3]>>) { | |
self.find.borrow_mut().unset(); | |
*self.find_progress.borrow_mut() = FindProgress::Started; | |
self.get_inlay_hints(); | |
self.get_semantic_styles(); | |
self.clear_style_cache(); | |
- self.trigger_syntax_change(delta); | |
+ self.trigger_syntax_change(deltas); | |
self.trigger_head_change(); | |
self.notify_special(); | |
} | |
@@ -936,32 +933,15 @@ impl Document { | |
self.text_layouts.borrow_mut().clear(); | |
} | |
- pub fn trigger_syntax_change(&self, delta: Option<&RopeDelta>) { | |
- if let Some(syntax) = self.syntax.clone() { | |
- let content = self.content.clone(); | |
+ pub fn trigger_syntax_change( | |
+ &mut self, | |
+ deltas: Option<SmallVec<[RopeDelta; 3]>>, | |
+ ) { | |
+ if let Some(syntax) = self.syntax.as_mut() { | |
let rev = self.buffer.rev(); | |
let text = self.buffer.text().clone(); | |
- let delta = delta.cloned(); | |
- let atomic_rev = self.buffer.atomic_rev(); | |
- let event_sink = self.event_sink.clone(); | |
- let tab_id = self.tab_id; | |
- rayon::spawn(move || { | |
- if atomic_rev.load(atomic::Ordering::Acquire) != rev { | |
- return; | |
- } | |
- let new_syntax = syntax.parse(rev, text, delta); | |
- if atomic_rev.load(atomic::Ordering::Acquire) != rev { | |
- return; | |
- } | |
- let _ = event_sink.submit_command( | |
- LAPCE_UI_COMMAND, | |
- LapceUICommand::UpdateSyntax { | |
- content, | |
- syntax: SingleUse::new(new_syntax), | |
- }, | |
- Target::Widget(tab_id), | |
- ); | |
- }); | |
+ | |
+ syntax.parse(rev, text, deltas.as_deref()); | |
} | |
} | |
@@ -1066,12 +1046,11 @@ impl Document { | |
} | |
} | |
- let delta = if deltas.len() == 1 { | |
- Some(&deltas[0].0) | |
- } else { | |
- None | |
- }; | |
- self.on_update(delta); | |
+ // TODO(minor): We could avoid this potential allocation since most apply_delta callers are actually using a Vec | |
+ // which we could reuse. | |
+ // We use a smallvec because there is unlikely to be more than a couple of deltas | |
+ let deltas_iter = deltas.iter().map(|(delta, _)| delta.clone()).collect(); | |
+ self.on_update(Some(deltas_iter)); | |
} | |
pub fn do_insert( | |
diff --git a/lapce-data/src/editor.rs b/lapce-data/src/editor.rs | |
index 6a07f9056..8612f9722 100644 | |
--- a/lapce-data/src/editor.rs | |
+++ b/lapce-data/src/editor.rs | |
@@ -748,19 +748,21 @@ impl LapceEditorBufferData { | |
is_inside: bool, | |
within_scroll: bool, | |
) -> bool { | |
- let hover = Arc::make_mut(&mut self.hover); | |
- | |
- if hover.status != HoverStatus::Inactive { | |
+ if self.hover.status != HoverStatus::Inactive { | |
if !is_inside || !within_scroll { | |
+ let hover = Arc::make_mut(&mut self.hover); | |
hover.cancel(); | |
return false; | |
} | |
let start_offset = self.doc.buffer().prev_code_boundary(offset); | |
- if self.doc.id() == hover.buffer_id && start_offset == hover.offset { | |
+ if self.doc.id() == self.hover.buffer_id | |
+ && start_offset == self.hover.offset | |
+ { | |
return true; | |
} | |
+ let hover = Arc::make_mut(&mut self.hover); | |
hover.cancel(); | |
return false; | |
} | |
diff --git a/lapce-data/src/history.rs b/lapce-data/src/history.rs | |
index e46a52026..b04f4cf6a 100644 | |
--- a/lapce-data/src/history.rs | |
+++ b/lapce-data/src/history.rs | |
@@ -266,9 +266,8 @@ impl DocumentHistory { | |
let content = self.buffer.as_ref().unwrap().text().clone(); | |
rayon::spawn(move || { | |
- if let Some(syntax) = | |
- Syntax::init(&path).map(|s| s.parse(0, content, None)) | |
- { | |
+ if let Some(mut syntax) = Syntax::init(&path) { | |
+ syntax.parse(0, content, None); | |
if let Some(styles) = syntax.styles { | |
let _ = event_sink.submit_command( | |
LAPCE_UI_COMMAND, | |
diff --git a/lapce-data/src/keypress/mod.rs b/lapce-data/src/keypress/mod.rs | |
index 754f4df13..b3b43908a 100644 | |
--- a/lapce-data/src/keypress/mod.rs | |
+++ b/lapce-data/src/keypress/mod.rs | |
@@ -61,7 +61,7 @@ pub fn paint_key( | |
.build() | |
.unwrap(); | |
let text_size = text_layout.size(); | |
- let text_layout_point = origin + (5.0, -(text_size.height / 2.0)); | |
+ let text_layout_point = origin + (5.0, -text_layout.cap_center()); | |
let rect = Size::new(text_size.width, 0.0) | |
.to_rect() | |
.with_origin(origin + (5.0, 0.0)) | |
diff --git a/lapce-data/src/markdown/mod.rs b/lapce-data/src/markdown/mod.rs | |
index 873b1da1b..9ea98cfdf 100644 | |
--- a/lapce-data/src/markdown/mod.rs | |
+++ b/lapce-data/src/markdown/mod.rs | |
@@ -78,8 +78,9 @@ pub fn parse_markdown(text: &str, config: &Config) -> RichText { | |
let syntax = language.map(Syntax::from_language); | |
- let styles = syntax.and_then(|syntax| { | |
- syntax.parse(0, Rope::from(&last_text), None).styles | |
+ let styles = syntax.and_then(|mut syntax| { | |
+ syntax.parse(0, Rope::from(&last_text), None); | |
+ syntax.styles | |
}); | |
if let Some(styles) = styles { | |
diff --git a/lapce-data/src/settings.rs b/lapce-data/src/settings.rs | |
index 3b06e7539..7083a0c64 100644 | |
--- a/lapce-data/src/settings.rs | |
+++ b/lapce-data/src/settings.rs | |
@@ -1,3 +1,5 @@ | |
+use std::sync::Arc; | |
+ | |
use druid::{Command, Env, EventCtx, Modifiers, Target, WidgetId}; | |
use lapce_core::{ | |
command::{EditCommand, FocusCommand, MoveCommand}, | |
@@ -7,6 +9,7 @@ use serde::{Deserialize, Serialize}; | |
use crate::{ | |
command::{CommandExecuted, CommandKind, LapceUICommand, LAPCE_UI_COMMAND}, | |
+ config::Config, | |
data::LapceMainSplitData, | |
keypress::KeyPressFocus, | |
split::SplitDirection, | |
@@ -98,6 +101,7 @@ pub struct LapceSettingsFocusData { | |
pub widget_id: WidgetId, | |
pub editor_tab_id: WidgetId, | |
pub main_split: LapceMainSplitData, | |
+ pub config: Arc<Config>, | |
} | |
impl KeyPressFocus for LapceSettingsFocusData { | |
@@ -124,6 +128,7 @@ impl KeyPressFocus for LapceSettingsFocusData { | |
ctx, | |
self.editor_tab_id, | |
SplitDirection::Vertical, | |
+ &self.config, | |
); | |
} | |
FocusCommand::SplitClose => { | |
diff --git a/lapce-proxy/src/plugin/lsp.rs b/lapce-proxy/src/plugin/lsp.rs | |
index bfb365843..e2c898111 100644 | |
--- a/lapce-proxy/src/plugin/lsp.rs | |
+++ b/lapce-proxy/src/plugin/lsp.rs | |
@@ -155,6 +155,7 @@ impl LspClient { | |
let server = match server_uri.scheme() { | |
"file" => { | |
let path = server_uri.to_file_path().map_err(|_| anyhow!(""))?; | |
+ #[cfg(unix)] | |
let _ = std::process::Command::new("chmod") | |
.arg("+x") | |
.arg(&path) | |
@@ -216,7 +217,7 @@ impl LspClient { | |
} | |
core_rpc.log( | |
log::Level::Error, | |
- format!("lsp server stderr: {line}"), | |
+ format!("lsp server stderr: {}", line.trim_end()), | |
); | |
} | |
Err(_) => { | |
diff --git a/lapce-proxy/src/plugin/psp.rs b/lapce-proxy/src/plugin/psp.rs | |
index 075838d1d..f924f0f8b 100644 | |
--- a/lapce-proxy/src/plugin/psp.rs | |
+++ b/lapce-proxy/src/plugin/psp.rs | |
@@ -789,7 +789,7 @@ impl PluginHostHandler { | |
volt_id, | |
pwd, | |
params.server_uri, | |
- Vec::new(), | |
+ params.server_args, | |
params.options, | |
); | |
}); | |
diff --git a/lapce-proxy/src/terminal.rs b/lapce-proxy/src/terminal.rs | |
index fb712a198..f98661514 100644 | |
--- a/lapce-proxy/src/terminal.rs | |
+++ b/lapce-proxy/src/terminal.rs | |
@@ -55,14 +55,38 @@ impl Terminal { | |
BaseDirs::new().map(|d| PathBuf::from(d.home_dir())) | |
}; | |
let shell = shell.trim(); | |
- if !shell.is_empty() { | |
+ let inside_flatpak = is_inside_flatpak(); | |
+ | |
+ if !shell.is_empty() || inside_flatpak { | |
let mut parts = shell.split(' '); | |
- let program = parts.next().unwrap(); | |
- if let Ok(p) = which::which(program) { | |
+ | |
+ if inside_flatpak { | |
+ let flatpak_spawn_path = "/usr/bin/flatpak-spawn".to_string(); | |
+ let host_shell = flatpak_get_default_host_shell(); | |
+ | |
+ let args = if shell.is_empty() { | |
+ vec!["--host".to_string(), host_shell] | |
+ } else { | |
+ vec![ | |
+ "--host".to_string(), | |
+ host_shell, | |
+ "-c".to_string(), | |
+ shell.to_string(), | |
+ ] | |
+ }; | |
+ | |
config.pty_config.shell = Some(Program::WithArgs { | |
- program: p.to_str().unwrap().to_string(), | |
- args: parts.map(|p| p.to_string()).collect::<Vec<String>>(), | |
+ program: flatpak_spawn_path, | |
+ args, | |
}) | |
+ } else { | |
+ let program = parts.next().unwrap(); | |
+ if let Ok(p) = which::which(program) { | |
+ config.pty_config.shell = Some(Program::WithArgs { | |
+ program: p.to_str().unwrap().to_string(), | |
+ args: parts.map(|p| p.to_string()).collect::<Vec<String>>(), | |
+ }) | |
+ } | |
} | |
} | |
setup_env(&config); | |
@@ -307,3 +331,52 @@ fn set_locale_environment() { | |
.replace('-', "_"); | |
std::env::set_var("LC_ALL", locale + ".UTF-8"); | |
} | |
+ | |
+#[cfg(not(target_os = "linux"))] | |
+fn flatpak_get_default_host_shell() -> String { | |
+ panic!( | |
+ "This should never be reached. If it is, ensure you don't have a file | |
+ called .flatpak-info in your root directory" | |
+ ); | |
+} | |
+ | |
+#[cfg(target_os = "linux")] | |
+fn flatpak_get_default_host_shell() -> String { | |
+ use std::process::Command; | |
+ | |
+ let env_string = Command::new("flatpak-spawn") | |
+ .arg("--host") | |
+ .arg("printenv") | |
+ .output() | |
+ .unwrap() | |
+ .stdout; | |
+ | |
+ let env_string = String::from_utf8(env_string).unwrap(); | |
+ | |
+ for env_pair in env_string.split('\n') { | |
+ let name_value: Vec<&str> = env_pair.split('=').collect(); | |
+ | |
+ if name_value[0] == "SHELL" { | |
+ return name_value[1].to_string(); | |
+ } | |
+ } | |
+ | |
+ // In case SHELL isn't set for whatever reason, fall back to this | |
+ "/bin/sh".to_string() | |
+} | |
+ | |
+#[cfg(not(target_os = "linux"))] | |
+fn is_inside_flatpak() -> bool { | |
+ false // Flatpak is only available on Linux | |
+} | |
+ | |
+#[cfg(target_os = "linux")] | |
+fn is_inside_flatpak() -> bool { | |
+ use std::path::Path; | |
+ | |
+ const FLATPAK_INFO_PATH: &str = "/.flatpak-info"; | |
+ | |
+ /* The de-facto way of checking whether one is inside of a Flatpak container is by checking for | |
+ the presence of /.flatpak-info in the filesystem */ | |
+ Path::new(FLATPAK_INFO_PATH).exists() | |
+} | |
diff --git a/lapce-ui/Cargo.toml b/lapce-ui/Cargo.toml | |
index e80e4a80b..ea2587360 100644 | |
--- a/lapce-ui/Cargo.toml | |
+++ b/lapce-ui/Cargo.toml | |
@@ -94,6 +94,9 @@ all-languages = [ | |
"lang-yaml", | |
"lang-julia", | |
"lang-wgsl", | |
+ "lang-dockerfile", | |
+ "lang-csharp", | |
+ "lang-nix", | |
] | |
lang-rust = ["lapce-core/lang-rust"] | |
@@ -127,3 +130,6 @@ lang-bash = ["lapce-core/lang-bash"] | |
lang-yaml = ["lapce-core/lang-yaml"] | |
lang-julia = ["lapce-core/lang-julia"] | |
lang-wgsl = ["lapce-core/lang-wgsl"] | |
+lang-dockerfile = ["lapce-core/lang-dockerfile"] | |
+lang-csharp = ["lapce-core/lang-csharp"] | |
+lang-nix = ["lapce-core/lang-nix"] | |
diff --git a/lapce-ui/src/about.rs b/lapce-ui/src/about.rs | |
index 05656fa65..f17212a3d 100644 | |
--- a/lapce-ui/src/about.rs | |
+++ b/lapce-ui/src/about.rs | |
@@ -195,10 +195,8 @@ impl Widget<LapceTabData> for AboutBoxContent { | |
self.mouse_pos = mouse_event.pos; | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
- ctx.request_paint(); | |
} else { | |
ctx.clear_cursor(); | |
- ctx.request_paint(); | |
} | |
ctx.set_handled(); | |
} | |
@@ -232,7 +230,7 @@ impl Widget<LapceTabData> for AboutBoxContent { | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.focus), | |
+ Target::Widget(*data.focus), | |
)); | |
ctx.set_handled(); | |
} | |
diff --git a/lapce-ui/src/alert.rs b/lapce-ui/src/alert.rs | |
index 71df51c2e..35b81ab33 100644 | |
--- a/lapce-ui/src/alert.rs | |
+++ b/lapce-ui/src/alert.rs | |
@@ -176,10 +176,8 @@ impl Widget<LapceTabData> for AlertBoxContent { | |
Event::MouseMove(mouse_event) => { | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
- ctx.request_paint(); | |
} else { | |
ctx.clear_cursor(); | |
- ctx.request_paint(); | |
} | |
} | |
Event::MouseDown(mouse_event) => { | |
@@ -231,7 +229,7 @@ impl Widget<LapceTabData> for AlertBoxContent { | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.focus), | |
+ Target::Widget(*data.focus), | |
)); | |
ctx.set_handled(); | |
} | |
@@ -399,7 +397,7 @@ impl Widget<LapceTabData> for AlertBoxContent { | |
.unwrap(); | |
let text_layout_size = text_layout.size(); | |
let point = self.buttons[i].center() | |
- - (text_layout_size.width / 2.0, text_layout_size.height / 2.0); | |
+ - (text_layout_size.width / 2.0, text_layout.cap_center()); | |
ctx.draw_text(&text_layout, point); | |
} | |
@@ -424,7 +422,7 @@ impl Widget<LapceTabData> for AlertBoxContent { | |
.unwrap(); | |
let text_layout_size = text_layout.size(); | |
let cancel_point = self.cancel_rect.center() | |
- - (text_layout_size.width / 2.0, text_layout_size.height / 2.0); | |
+ - (text_layout_size.width / 2.0, text_layout.cap_center()); | |
ctx.draw_text(&text_layout, cancel_point); | |
} | |
} | |
diff --git a/lapce-ui/src/app.rs b/lapce-ui/src/app.rs | |
index d6f22226c..7cf3011bc 100644 | |
--- a/lapce-ui/src/app.rs | |
+++ b/lapce-ui/src/app.rs | |
@@ -333,11 +333,11 @@ impl AppDelegate<LapceData> for LapceAppDelegate { | |
); | |
let mut tab = meta.data; | |
- tab.window_id = window_data.window_id; | |
+ tab.window_id = Arc::new(window_data.window_id); | |
let tab_id = tab.id; | |
window_data.tabs_order = Arc::new(vec![tab_id]); | |
- window_data.active_id = tab_id; | |
+ window_data.active_id = Arc::new(tab_id); | |
window_data.tabs.clear(); | |
window_data.tabs.insert(tab_id, tab); | |
diff --git a/lapce-ui/src/completion.rs b/lapce-ui/src/completion.rs | |
index 29fa697c9..8c05e8f30 100644 | |
--- a/lapce-ui/src/completion.rs | |
+++ b/lapce-ui/src/completion.rs | |
@@ -345,50 +345,50 @@ impl Widget<LapceTabData> for CompletionContainer { | |
{ | |
ctx.request_layout(); | |
} | |
- } | |
- if old_data.completion.input != data.completion.input | |
- || old_data.completion.request_id != data.completion.request_id | |
- || old_data.completion.status != data.completion.status | |
- || !old_data | |
- .completion | |
- .current_items() | |
- .same(data.completion.current_items()) | |
- || !old_data | |
- .completion | |
- .filtered_items | |
- .same(&data.completion.filtered_items) | |
- { | |
- self.update_documentation(data); | |
- ctx.request_layout(); | |
- } | |
+ if old_data.completion.input != data.completion.input | |
+ || old_data.completion.request_id != data.completion.request_id | |
+ || old_data.completion.status != data.completion.status | |
+ || !old_data | |
+ .completion | |
+ .current_items() | |
+ .same(data.completion.current_items()) | |
+ || !old_data | |
+ .completion | |
+ .filtered_items | |
+ .same(&data.completion.filtered_items) | |
+ { | |
+ self.update_documentation(data); | |
+ ctx.request_layout(); | |
+ } | |
- if (old_completion.status == CompletionStatus::Inactive | |
- && completion.status != CompletionStatus::Inactive) | |
- || (old_completion.input != completion.input) | |
- { | |
- ctx.submit_command(Command::new( | |
- LAPCE_UI_COMMAND, | |
- LapceUICommand::ResetFade, | |
- Target::Widget(self.scroll_id), | |
- )); | |
- } | |
+ if (old_completion.status == CompletionStatus::Inactive | |
+ && completion.status != CompletionStatus::Inactive) | |
+ || (old_completion.input != completion.input) | |
+ { | |
+ ctx.submit_command(Command::new( | |
+ LAPCE_UI_COMMAND, | |
+ LapceUICommand::ResetFade, | |
+ Target::Widget(self.scroll_id), | |
+ )); | |
+ } | |
- if old_completion.index != completion.index { | |
- self.ensure_item_visible(ctx, data, env); | |
- self.update_documentation(data); | |
- ctx.request_paint(); | |
- } | |
+ if old_completion.index != completion.index { | |
+ self.ensure_item_visible(ctx, data, env); | |
+ self.update_documentation(data); | |
+ ctx.request_paint(); | |
+ } | |
- if self | |
- .documentation | |
- .widget_mut() | |
- .inner_mut() | |
- .child_mut() | |
- .doc_layout | |
- .needs_rebuild_after_update(ctx) | |
- { | |
- ctx.request_layout(); | |
+ if self | |
+ .documentation | |
+ .widget_mut() | |
+ .inner_mut() | |
+ .child_mut() | |
+ .doc_layout | |
+ .needs_rebuild_after_update(ctx) | |
+ { | |
+ ctx.request_layout(); | |
+ } | |
} | |
} | |
@@ -542,8 +542,6 @@ impl Widget<LapceTabData> for Completion { | |
let item = &items[line]; | |
- let y = line_height * line as f64 + 5.0; | |
- | |
if let Some((svg, color)) = completion_svg(item.item.kind, &data.config) | |
{ | |
let color = color.unwrap_or_else(|| { | |
@@ -569,7 +567,6 @@ impl Widget<LapceTabData> for Completion { | |
let focus_color = | |
data.config.get_color_unchecked(LapceTheme::EDITOR_FOCUS); | |
let content = item.item.label.as_str(); | |
- let point = Point::new(line_height + 5.0, y); | |
let mut text_layout = ctx | |
.text() | |
@@ -597,6 +594,8 @@ impl Widget<LapceTabData> for Completion { | |
); | |
} | |
let text_layout = text_layout.build().unwrap(); | |
+ let y = line_height * line as f64 + text_layout.y_offset(line_height); | |
+ let point = Point::new(line_height + 5.0, y); | |
ctx.draw_text(&text_layout, point); | |
} | |
} | |
@@ -722,65 +721,3 @@ fn parse_documentation(doc: &Documentation, config: &Config) -> RichText { | |
}, | |
} | |
} | |
- | |
-#[derive(Clone)] | |
-pub struct CompletionState { | |
- pub widget_id: WidgetId, | |
- pub items: Vec<ScoredCompletionItem>, | |
- pub input: String, | |
- pub offset: usize, | |
- pub index: usize, | |
- pub scroll_offset: f64, | |
-} | |
- | |
-impl CompletionState { | |
- pub fn new() -> CompletionState { | |
- CompletionState { | |
- widget_id: WidgetId::next(), | |
- items: Vec::new(), | |
- input: "".to_string(), | |
- offset: 0, | |
- index: 0, | |
- scroll_offset: 0.0, | |
- } | |
- } | |
- | |
- pub fn len(&self) -> usize { | |
- self.items.iter().filter(|i| i.score != 0).count() | |
- } | |
- | |
- pub fn is_empty(&self) -> bool { | |
- self.len() == 0 | |
- } | |
- | |
- pub fn current_items(&self) -> Vec<&ScoredCompletionItem> { | |
- self.items.iter().filter(|i| i.score != 0).collect() | |
- } | |
- | |
- pub fn clear(&mut self) { | |
- self.input = "".to_string(); | |
- self.items = Vec::new(); | |
- self.offset = 0; | |
- self.index = 0; | |
- self.scroll_offset = 0.0; | |
- } | |
- | |
- pub fn cancel(&mut self, ctx: &mut EventCtx) { | |
- self.clear(); | |
- self.request_paint(ctx); | |
- } | |
- | |
- pub fn request_paint(&self, ctx: &mut EventCtx) { | |
- ctx.submit_command(Command::new( | |
- LAPCE_UI_COMMAND, | |
- LapceUICommand::RequestPaint, | |
- Target::Widget(self.widget_id), | |
- )); | |
- } | |
-} | |
- | |
-impl Default for CompletionState { | |
- fn default() -> Self { | |
- Self::new() | |
- } | |
-} | |
diff --git a/lapce-ui/src/editor.rs b/lapce-ui/src/editor.rs | |
index 294ad8cc9..4033dbbae 100644 | |
--- a/lapce-ui/src/editor.rs | |
+++ b/lapce-ui/src/editor.rs | |
@@ -639,8 +639,7 @@ impl LapceEditor { | |
Point::new( | |
0.0, | |
line_height * l as f64 | |
- + (line_height - text_layout.text.size().height) | |
- / 2.0, | |
+ + text_layout.text.y_offset(line_height), | |
), | |
); | |
@@ -684,7 +683,7 @@ impl LapceEditor { | |
Point::new( | |
0.0, | |
line_height * line as f64 | |
- + (line_height - text_layout.size().height) / 2.0, | |
+ + text_layout.y_offset(line_height), | |
), | |
); | |
line += 1; | |
@@ -845,9 +844,7 @@ impl LapceEditor { | |
&text_layout, | |
Point::new( | |
0.0, | |
- (data.config.editor.line_height as f64 | |
- - text_layout.size().height) | |
- / 2.0, | |
+ text_layout.y_offset(data.config.editor.line_height as f64), | |
), | |
); | |
} | |
@@ -876,8 +873,7 @@ impl LapceEditor { | |
info.font_size, | |
&data.config, | |
); | |
- let y = | |
- info.y + (info.line_height - text_layout.text.size().height) / 2.0; | |
+ let y = info.y + text_layout.text.y_offset(info.line_height); | |
let height = text_layout.text.size().height; | |
for (x0, x1, style) in text_layout.extra_style.iter() { | |
if let Some(bg) = &style.bg_color { | |
@@ -1861,7 +1857,7 @@ impl Widget<LapceTabData> for LapceEditor { | |
} | |
fn paint(&mut self, ctx: &mut PaintCtx, data: &LapceTabData, env: &Env) { | |
- let is_focused = data.focus == self.view_id; | |
+ let is_focused = *data.focus == self.view_id; | |
let data = data.editor_view_content(self.view_id); | |
// TODO: u128 is supported by config-rs since 0.12.0, but also the API changed heavily, | |
diff --git a/lapce-ui/src/editor/gutter.rs b/lapce-ui/src/editor/gutter.rs | |
index 3dd7b025e..4916921b8 100644 | |
--- a/lapce-ui/src/editor/gutter.rs | |
+++ b/lapce-ui/src/editor/gutter.rs | |
@@ -192,12 +192,6 @@ impl LapceEditorGutter { | |
let actual_line = l - (line - len) + r.start; | |
let content = actual_line + 1; | |
- let x = ((last_line + 1).to_string().len() | |
- - content.to_string().len()) | |
- as f64 | |
- * width; | |
- let y = line_height * l as f64 + 5.0 - scroll_offset.y; | |
- let pos = Point::new(x, y); | |
let text_layout = ctx | |
.text() | |
@@ -221,6 +215,14 @@ impl LapceEditorGutter { | |
) | |
.build() | |
.unwrap(); | |
+ let x = ((last_line + 1).to_string().len() | |
+ - content.to_string().len()) | |
+ as f64 | |
+ * width; | |
+ let y = line_height * l as f64 | |
+ + text_layout.y_offset(line_height) | |
+ - scroll_offset.y; | |
+ let pos = Point::new(x, y); | |
ctx.draw_text(&text_layout, pos); | |
if l > end_line { | |
@@ -243,12 +245,6 @@ impl LapceEditorGutter { | |
let right_actual_line = l - (line - len) + r.start; | |
let left_content = left_actual_line + 1; | |
- let x = ((last_line + 1).to_string().len() | |
- - left_content.to_string().len()) | |
- as f64 | |
- * width; | |
- let y = line_height * l as f64 + 5.0 - scroll_offset.y; | |
- let pos = Point::new(x, y); | |
let text_layout = ctx | |
.text() | |
@@ -264,6 +260,14 @@ impl LapceEditorGutter { | |
) | |
.build() | |
.unwrap(); | |
+ let x = ((last_line + 1).to_string().len() | |
+ - left_content.to_string().len()) | |
+ as f64 | |
+ * width; | |
+ let y = line_height * l as f64 | |
+ + text_layout.y_offset(line_height) | |
+ - scroll_offset.y; | |
+ let pos = Point::new(x, y); | |
ctx.draw_text(&text_layout, pos); | |
let right_content = right_actual_line + 1; | |
@@ -346,14 +350,6 @@ impl LapceEditorGutter { | |
let actual_line = l - (line - len) + r.start; | |
let content = actual_line + 1; | |
- let x = ((last_line + 1).to_string().len() | |
- - content.to_string().len()) | |
- as f64 | |
- * width | |
- + self.width | |
- + 2.0 * width; | |
- let y = line_height * l as f64 + 5.0 - scroll_offset.y; | |
- let pos = Point::new(x, y); | |
let text_layout = ctx | |
.text() | |
@@ -375,6 +371,16 @@ impl LapceEditorGutter { | |
}) | |
.build() | |
.unwrap(); | |
+ let x = ((last_line + 1).to_string().len() | |
+ - content.to_string().len()) | |
+ as f64 | |
+ * width | |
+ + self.width | |
+ + 2.0 * width; | |
+ let y = line_height * l as f64 | |
+ + text_layout.y_offset(line_height) | |
+ - scroll_offset.y; | |
+ let pos = Point::new(x, y); | |
ctx.draw_text(&text_layout, pos); | |
if l > end_line { | |
@@ -464,7 +470,7 @@ impl LapceEditorGutter { | |
+ if is_small { | |
0.0 | |
} else { | |
- (line_height as f64 - text_layout.size().height) / 2.0 | |
+ text_layout.y_offset(line_height as f64) | |
}, | |
); | |
ctx.draw_text(&text_layout, pos); | |
@@ -580,7 +586,7 @@ impl LapceEditorGutter { | |
// Vertically centered | |
let y = line_height * line as f64 - scroll_offset.y | |
- + (line_height - text_layout.size().height) / 2.0; | |
+ + text_layout.y_offset(line_height); | |
ctx.draw_text(&text_layout, Point::new(x, y)); | |
} | |
diff --git a/lapce-ui/src/editor/header.rs b/lapce-ui/src/editor/header.rs | |
index 8052790e3..2536062c5 100644 | |
--- a/lapce-ui/src/editor/header.rs | |
+++ b/lapce-ui/src/editor/header.rs | |
@@ -1,7 +1,7 @@ | |
use std::{iter::Iterator, path::PathBuf}; | |
use druid::{ | |
- piet::{Text, TextAttribute, TextLayout as TextLayoutTrait, TextLayoutBuilder}, | |
+ piet::{Text, TextAttribute, TextLayoutBuilder}, | |
BoxConstraints, Command, Env, Event, EventCtx, LayoutCtx, LifeCycle, | |
LifeCycleCtx, MouseEvent, PaintCtx, Point, Rect, RenderContext, Size, Target, | |
UpdateCtx, Widget, WidgetId, | |
@@ -209,10 +209,7 @@ impl LapceEditorHeader { | |
let text_layout = text_layout.build().unwrap(); | |
ctx.draw_text( | |
&text_layout, | |
- Point::new( | |
- size.height, | |
- (size.height - text_layout.size().height) / 2.0, | |
- ), | |
+ Point::new(size.height, text_layout.y_offset(size.height)), | |
); | |
}); | |
} | |
@@ -254,10 +251,8 @@ impl Widget<LapceTabData> for LapceEditorHeader { | |
self.mouse_pos = mouse_event.pos; | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
- ctx.request_paint(); | |
} else { | |
ctx.clear_cursor(); | |
- ctx.request_paint(); | |
} | |
} | |
Event::MouseDown(mouse_event) => { | |
diff --git a/lapce-ui/src/editor/tab.rs b/lapce-ui/src/editor/tab.rs | |
index d3603d739..5ee382af2 100644 | |
--- a/lapce-ui/src/editor/tab.rs | |
+++ b/lapce-ui/src/editor/tab.rs | |
@@ -1,9 +1,9 @@ | |
use std::{cell::RefCell, rc::Rc, sync::Arc}; | |
use druid::{ | |
- kurbo::Line, piet::TextLayout, BoxConstraints, Command, Env, Event, EventCtx, | |
- LayoutCtx, LifeCycle, LifeCycleCtx, MouseEvent, PaintCtx, Point, Rect, | |
- RenderContext, Size, Target, UpdateCtx, Widget, WidgetId, WidgetPod, | |
+ kurbo::Line, BoxConstraints, Command, Env, Event, EventCtx, LayoutCtx, | |
+ LifeCycle, LifeCycleCtx, MouseEvent, PaintCtx, Point, Rect, RenderContext, Size, | |
+ Target, UpdateCtx, Widget, WidgetId, WidgetPod, | |
}; | |
use lapce_core::command::FocusCommand; | |
use lapce_data::{ | |
@@ -60,7 +60,7 @@ impl LapceEditorTab { | |
EditorTabChild::Editor(view_id, _, _) => { | |
data.main_split.editors.remove(view_id); | |
} | |
- EditorTabChild::Settings(_, _) => {} | |
+ EditorTabChild::Settings { .. } => {} | |
} | |
} | |
ctx.submit_command(Command::new( | |
@@ -128,7 +128,7 @@ impl LapceEditorTab { | |
EditorTabChild::Editor(view_id, _, _) => { | |
data.main_split.editors.remove(&view_id); | |
} | |
- EditorTabChild::Settings(_, _) => {} | |
+ EditorTabChild::Settings { .. } => {} | |
} | |
} | |
} | |
@@ -306,7 +306,6 @@ impl Widget<LapceTabData> for LapceEditorTab { | |
match event { | |
Event::MouseMove(mouse_event) => { | |
self.mouse_pos = mouse_event.pos; | |
- ctx.request_paint(); | |
} | |
Event::MouseUp(mouse_event) => { | |
self.mouse_up(ctx, data, mouse_event); | |
@@ -653,10 +652,9 @@ impl TabRectRenderer for TabRect { | |
); | |
} | |
ctx.draw_svg(&self.svg, rect, None); | |
- let text_size = self.text_layout.size(); | |
ctx.draw_text( | |
&self.text_layout, | |
- Point::new(rect.x1 + 5.0, (size.height - text_size.height) / 2.0), | |
+ Point::new(rect.x1 + 5.0, self.text_layout.y_offset(size.height)), | |
); | |
let x = self.rect.x1; | |
ctx.stroke( | |
@@ -697,7 +695,7 @@ impl TabRectRenderer for TabRect { | |
let doc = data.main_split.editor_doc(*editor_id); | |
doc.buffer().is_pristine() | |
} | |
- EditorTabChild::Settings(_, _) => true, | |
+ EditorTabChild::Settings { .. } => true, | |
}; | |
if !is_pristine { | |
diff --git a/lapce-ui/src/editor/tab_header.rs b/lapce-ui/src/editor/tab_header.rs | |
index 357adfc59..04804220f 100644 | |
--- a/lapce-ui/src/editor/tab_header.rs | |
+++ b/lapce-ui/src/editor/tab_header.rs | |
@@ -2,9 +2,9 @@ use std::time::Duration; | |
use druid::{ | |
kurbo::Line, | |
- piet::{Text, TextAttribute, TextLayout, TextLayoutBuilder}, | |
+ piet::{Text, TextAttribute, TextLayoutBuilder}, | |
BoxConstraints, Command, Env, Event, EventCtx, LayoutCtx, LifeCycle, | |
- LifeCycleCtx, MouseEvent, PaintCtx, Point, RenderContext, Size, Target, | |
+ LifeCycleCtx, MouseEvent, PaintCtx, Point, Rect, RenderContext, Size, Target, | |
TimerToken, UpdateCtx, Widget, WidgetId, WidgetPod, | |
}; | |
use lapce_core::command::FocusCommand; | |
@@ -33,6 +33,7 @@ pub struct LapceEditorTabHeader { | |
>, | |
icons: Vec<LapceIcon>, | |
mouse_pos: Point, | |
+ hover_rect: Option<Rect>, | |
is_hot: bool, | |
} | |
@@ -48,12 +49,14 @@ impl LapceEditorTabHeader { | |
icons: Vec::new(), | |
mouse_pos: Point::ZERO, | |
is_hot: false, | |
+ hover_rect: None, | |
} | |
} | |
- fn icon_hit_test(&self, mouse_event: &MouseEvent) -> bool { | |
+ fn icon_hit_test(&mut self, mouse_event: &MouseEvent) -> bool { | |
for icon in self.icons.iter() { | |
if icon.rect.contains(mouse_event.pos) { | |
+ self.hover_rect = Some(icon.rect); | |
return true; | |
} | |
} | |
@@ -129,7 +132,7 @@ impl LapceEditorTabHeader { | |
text = format!("{text} (Working tree)"); | |
} | |
} | |
- EditorTabChild::Settings(_, _) => { | |
+ EditorTabChild::Settings { .. } => { | |
text = "Settings".to_string(); | |
hint = format!("ver. {}", *VERSION); | |
} | |
@@ -172,10 +175,7 @@ impl LapceEditorTabHeader { | |
let text_layout = text_layout.build().unwrap(); | |
ctx.draw_text( | |
&text_layout, | |
- Point::new( | |
- svg_rect.x1 + 5.0, | |
- (size.height - text_layout.size().height) / 2.0, | |
- ), | |
+ Point::new(svg_rect.x1 + 5.0, text_layout.y_offset(size.height)), | |
); | |
} | |
} | |
@@ -191,12 +191,16 @@ impl Widget<LapceTabData> for LapceEditorTabHeader { | |
match event { | |
Event::MouseMove(mouse_event) => { | |
self.mouse_pos = mouse_event.pos; | |
+ let hover_rect = self.hover_rect; | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
} else { | |
+ self.hover_rect = None; | |
ctx.clear_cursor(); | |
} | |
- ctx.request_paint(); | |
+ if hover_rect != self.hover_rect { | |
+ ctx.request_paint(); | |
+ } | |
} | |
Event::MouseDown(mouse_event) => { | |
self.mouse_down(ctx, mouse_event); | |
diff --git a/lapce-ui/src/editor/tab_header_content.rs b/lapce-ui/src/editor/tab_header_content.rs | |
index cb9b20d48..fded32bc8 100644 | |
--- a/lapce-ui/src/editor/tab_header_content.rs | |
+++ b/lapce-ui/src/editor/tab_header_content.rs | |
@@ -58,6 +58,9 @@ impl LapceEditorTabHeaderContent { | |
} | |
fn cancel_pending_drag(&mut self, data: &mut LapceTabData) { | |
+ if data.drag.is_none() { | |
+ return; | |
+ } | |
*Arc::make_mut(&mut data.drag) = None; | |
} | |
@@ -133,7 +136,6 @@ impl LapceEditorTabHeaderContent { | |
} else { | |
ctx.clear_cursor(); | |
} | |
- ctx.request_paint(); | |
if !mouse_event.buttons.contains(MouseButton::Left) { | |
// If drag data exists, mouse was released outside of the view. | |
@@ -369,7 +371,7 @@ impl Widget<LapceTabData> for LapceEditorTabHeaderContent { | |
text = editor.content.file_name().to_string(); | |
} | |
} | |
- EditorTabChild::Settings(_, _) => { | |
+ EditorTabChild::Settings { .. } => { | |
text = format!("Settings (ver. {})", *VERSION); | |
} | |
} | |
diff --git a/lapce-ui/src/editor/view.rs b/lapce-ui/src/editor/view.rs | |
index 2243afc7c..06cc9e21a 100644 | |
--- a/lapce-ui/src/editor/view.rs | |
+++ b/lapce-ui/src/editor/view.rs | |
@@ -54,9 +54,17 @@ pub fn editor_tab_child_widget( | |
EditorTabChild::Editor(view_id, editor_id, find_view_id) => { | |
LapceEditorView::new(*view_id, *editor_id, *find_view_id).boxed() | |
} | |
- EditorTabChild::Settings(widget_id, editor_tab_id) => { | |
- LapceSettingsPanel::new(data, *widget_id, *editor_tab_id).boxed() | |
- } | |
+ EditorTabChild::Settings { | |
+ settings_widget_id, | |
+ editor_tab_id, | |
+ keymap_input_view_id, | |
+ } => LapceSettingsPanel::new( | |
+ data, | |
+ *settings_widget_id, | |
+ *editor_tab_id, | |
+ *keymap_input_view_id, | |
+ ) | |
+ .boxed(), | |
} | |
} | |
@@ -127,7 +135,7 @@ impl LapceEditorView { | |
if left_click { | |
ctx.request_focus(); | |
} | |
- data.focus = self.view_id; | |
+ data.focus = Arc::new(self.view_id); | |
let editor = data.main_split.editors.get(&self.view_id).unwrap().clone(); | |
if let Some(editor_tab_id) = editor.tab_id { | |
let editor_tab = | |
@@ -814,8 +822,8 @@ impl Widget<LapceTabData> for LapceEditorView { | |
} | |
} | |
- if data.config.editor.blink_interval > 0 && data.focus == self.view_id { | |
- let reset = if old_data.focus != self.view_id { | |
+ if data.config.editor.blink_interval > 0 && *data.focus == self.view_id { | |
+ let reset = if *old_data.focus != self.view_id { | |
true | |
} else { | |
let mode = editor_data.editor.cursor.get_mode(); | |
diff --git a/lapce-ui/src/explorer.rs b/lapce-ui/src/explorer.rs | |
index 1d9333743..199f462db 100644 | |
--- a/lapce-ui/src/explorer.rs | |
+++ b/lapce-ui/src/explorer.rs | |
@@ -4,7 +4,7 @@ use std::{collections::HashMap, path::Path}; | |
use druid::menu::MenuEventCtx; | |
use druid::piet::TextAttribute; | |
use druid::{ | |
- piet::{Text, TextLayout as PietTextLayout, TextLayoutBuilder}, | |
+ piet::{Text, TextLayoutBuilder}, | |
BoxConstraints, Command, Cursor, Env, Event, EventCtx, LayoutCtx, LifeCycle, | |
LifeCycleCtx, PaintCtx, Point, Rect, RenderContext, Size, Target, UpdateCtx, | |
Widget, WidgetExt, WidgetId, WidgetPod, | |
@@ -126,10 +126,7 @@ fn paint_single_file_node_item( | |
.unwrap(); | |
ctx.draw_text( | |
&text_layout, | |
- Point::new( | |
- 38.0 + padding, | |
- y + (line_height - text_layout.size().height) / 2.0, | |
- ), | |
+ Point::new(38.0 + padding, y + text_layout.y_offset(line_height)), | |
); | |
} | |
@@ -607,7 +604,7 @@ impl Widget<LapceTabData> for FileExplorerFileList { | |
// The ids are so that the correct LapceTabData can be acquired inside the menu event cb | |
// since the context menu only gets access to LapceData | |
- let window_id = data.window_id; | |
+ let window_id = *data.window_id; | |
let tab_id = data.id; | |
let item = druid::MenuItem::new("New File").on_activate( | |
make_new_file_cb( | |
@@ -957,6 +954,7 @@ struct OpenEditorList { | |
mouse_pos: Point, | |
in_view_tab_children: HashMap<usize, (Rect, WidgetId)>, | |
mouse_down: Option<(usize, Option<Rect>)>, | |
+ hover_index: Option<usize>, | |
} | |
impl OpenEditorList { | |
@@ -966,6 +964,7 @@ impl OpenEditorList { | |
mouse_pos: Point::ZERO, | |
in_view_tab_children: HashMap::new(), | |
mouse_down: None, | |
+ hover_index: None, | |
} | |
} | |
@@ -1015,7 +1014,7 @@ impl OpenEditorList { | |
text = format!("{text} (Working tree)"); | |
} | |
} | |
- EditorTabChild::Settings(_, _) => { | |
+ EditorTabChild::Settings { .. } => { | |
text = "Settings".to_string(); | |
hint = format!("ver. {}", *VERSION); | |
} | |
@@ -1108,8 +1107,7 @@ impl OpenEditorList { | |
&text_layout, | |
Point::new( | |
svg_rect.x1 + 5.0, | |
- i as f64 * self.line_height | |
- + (self.line_height - text_layout.size().height) / 2.0, | |
+ i as f64 * self.line_height + text_layout.y_offset(self.line_height), | |
), | |
); | |
} | |
@@ -1125,13 +1123,19 @@ impl Widget<LapceTabData> for OpenEditorList { | |
) { | |
match event { | |
Event::MouseMove(mouse_event) => { | |
+ self.mouse_pos = mouse_event.pos; | |
let index = (mouse_event.pos.y / self.line_height).floor() as usize; | |
+ let hover_index = self.hover_index; | |
if self.in_view_tab_children.contains_key(&index) { | |
+ self.hover_index = Some(index); | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
} else { | |
+ self.hover_index = None; | |
ctx.clear_cursor(); | |
} | |
- self.mouse_pos = mouse_event.pos; | |
+ if hover_index != self.hover_index { | |
+ ctx.request_paint(); | |
+ } | |
} | |
Event::MouseDown(mouse_event) => { | |
self.mouse_down = None; | |
@@ -1269,8 +1273,7 @@ impl Widget<LapceTabData> for OpenEditorList { | |
Point::new( | |
10.0, | |
(i as f64 * self.line_height) | |
- + (self.line_height - text_layout.size().height) | |
- / 2.0, | |
+ + text_layout.y_offset(self.line_height), | |
), | |
); | |
} | |
diff --git a/lapce-ui/src/find.rs b/lapce-ui/src/find.rs | |
index d5d0d815c..a19f9a7b4 100644 | |
--- a/lapce-ui/src/find.rs | |
+++ b/lapce-ui/src/find.rs | |
@@ -17,7 +17,6 @@ pub struct FindBox { | |
parent_view_id: WidgetId, | |
input_width: f64, | |
result_width: f64, | |
- result_pos: Point, | |
input: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>, | |
icons: Vec<LapceIcon>, | |
mouse_pos: Point, | |
@@ -75,7 +74,6 @@ impl FindBox { | |
parent_view_id, | |
input_width: 200.0, | |
result_width: 75.0, | |
- result_pos: Point::ZERO, | |
input: WidgetPod::new(input.boxed()), | |
icons, | |
mouse_pos: Point::ZERO, | |
@@ -115,10 +113,8 @@ impl Widget<LapceTabData> for FindBox { | |
self.mouse_pos = mouse_event.pos; | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
- ctx.request_paint(); | |
} else { | |
ctx.clear_cursor(); | |
- ctx.request_paint(); | |
} | |
} | |
Event::MouseDown(mouse_event) => { | |
@@ -162,11 +158,6 @@ impl Widget<LapceTabData> for FindBox { | |
.inflate(-5.0, -5.0); | |
} | |
- self.result_pos = Point::new( | |
- input_size.width, | |
- (height - data.config.ui.font_size() as f64) / 2.0, | |
- ); | |
- | |
Size::new(width, height) | |
} | |
@@ -263,7 +254,11 @@ impl Widget<LapceTabData> for FindBox { | |
.build() | |
.unwrap(); | |
- ctx.draw_text(&text_layout, self.result_pos); | |
+ let input_size = self.input.layout_rect().size(); | |
+ ctx.draw_text( | |
+ &text_layout, | |
+ Point::new(input_size.width, text_layout.y_offset(input_size.height)), | |
+ ); | |
for icon in self.icons.iter() { | |
if icon.rect.contains(self.mouse_pos) { | |
diff --git a/lapce-ui/src/keymap.rs b/lapce-ui/src/keymap.rs | |
index 99abbfbe1..e61e89d4e 100644 | |
--- a/lapce-ui/src/keymap.rs | |
+++ b/lapce-ui/src/keymap.rs | |
@@ -28,9 +28,9 @@ pub struct LapceKeymap { | |
} | |
impl LapceKeymap { | |
- pub fn new_split(data: &LapceTabData) -> LapceSplit { | |
+ pub fn new_split(keymap_input_view_id: WidgetId) -> LapceSplit { | |
let keymap = Self { | |
- widget_id: data.settings.keymap_widget_id, | |
+ widget_id: WidgetId::next(), | |
active_keymap: None, | |
line_height: 35.0, | |
keymap_confirm: Rect::ZERO, | |
@@ -38,16 +38,13 @@ impl LapceKeymap { | |
}; | |
let keymap = LapceScroll::new(keymap); | |
- let input = LapceEditorView::new( | |
- data.settings.keymap_view_id, | |
- WidgetId::next(), | |
- None, | |
- ) | |
- .hide_header() | |
- .hide_gutter() | |
- .padding((15.0, 15.0)); | |
+ let input = | |
+ LapceEditorView::new(keymap_input_view_id, WidgetId::next(), None) | |
+ .hide_header() | |
+ .hide_gutter() | |
+ .padding((15.0, 15.0)); | |
let header = LapceKeymapHeader::new(); | |
- let split = LapceSplit::new(data.settings.keymap_split_id) | |
+ let split = LapceSplit::new(WidgetId::next()) | |
.horizontal() | |
.with_child(input.boxed(), None, 100.0) | |
.with_child(header.boxed(), None, 100.0) | |
@@ -106,7 +103,7 @@ impl LapceKeymap { | |
} | |
fn request_focus(&self, ctx: &mut EventCtx, data: &mut LapceTabData) { | |
- data.focus = self.widget_id; | |
+ data.focus = Arc::new(self.widget_id); | |
ctx.request_focus(); | |
} | |
} | |
@@ -290,13 +287,12 @@ impl Widget<LapceTabData> for LapceKeymap { | |
) | |
.build() | |
.unwrap(); | |
- let text_size = text_layout.size(); | |
ctx.draw_text( | |
&text_layout, | |
Point::new( | |
10.0, | |
i as f64 * self.line_height | |
- + (self.line_height - text_size.height) / 2.0, | |
+ + text_layout.y_offset(self.line_height), | |
), | |
); | |
}); | |
@@ -323,7 +319,6 @@ impl Widget<LapceTabData> for LapceKeymap { | |
) | |
.build() | |
.unwrap(); | |
- let text_size = text_layout.size(); | |
ctx.draw_text( | |
&text_layout, | |
Point::new( | |
@@ -335,7 +330,7 @@ impl Widget<LapceTabData> for LapceKeymap { | |
0.0 | |
}, | |
i as f64 * self.line_height | |
- + (self.line_height - text_size.height) / 2.0, | |
+ + text_layout.y_offset(self.line_height), | |
), | |
) | |
} | |
diff --git a/lapce-ui/src/palette.rs b/lapce-ui/src/palette.rs | |
index 1c383d957..7613a62e4 100644 | |
--- a/lapce-ui/src/palette.rs | |
+++ b/lapce-ui/src/palette.rs | |
@@ -186,7 +186,6 @@ impl Widget<LapceTabData> for Palette { | |
) { | |
if !old_data.palette.same(&data.palette) { | |
ctx.request_layout(); | |
- ctx.request_paint(); | |
} | |
self.container.update(ctx, data, env); | |
@@ -763,8 +762,7 @@ impl PaletteContent { | |
let text_layout = text_layout.build().unwrap(); | |
let x = svg_x + 5.0; | |
- let y = line_height * line as f64 | |
- + (line_height - text_layout.size().height) / 2.0; | |
+ let y = line_height * line as f64 + text_layout.y_offset(line_height); | |
let point = Point::new(x, y); | |
ctx.draw_text(&text_layout, point); | |
} | |
diff --git a/lapce-ui/src/panel.rs b/lapce-ui/src/panel.rs | |
index f1dfa884e..63a0df948 100644 | |
--- a/lapce-ui/src/panel.rs | |
+++ b/lapce-ui/src/panel.rs | |
@@ -2,7 +2,7 @@ use std::{collections::HashMap, sync::Arc}; | |
use druid::{ | |
kurbo::Line, | |
- piet::{Text, TextLayout, TextLayoutBuilder, TextStorage}, | |
+ piet::{Text, TextLayoutBuilder, TextStorage}, | |
BoxConstraints, Command, Cursor, Env, Event, EventCtx, LayoutCtx, LifeCycle, | |
LifeCycleCtx, PaintCtx, Point, Rect, RenderContext, Size, Target, UpdateCtx, | |
Widget, WidgetExt, WidgetId, WidgetPod, | |
@@ -356,7 +356,7 @@ impl Widget<LapceTabData> for PanelSectionHeader { | |
.build() | |
.unwrap(); | |
let height = ctx.size().height; | |
- let y = (height - text_layout.size().height) / 2.0; | |
+ let y = text_layout.y_offset(height); | |
ctx.draw_text(&text_layout, Point::new(10.0, y)); | |
}); | |
} | |
diff --git a/lapce-ui/src/picker.rs b/lapce-ui/src/picker.rs | |
index e0955c5be..708f9ef47 100644 | |
--- a/lapce-ui/src/picker.rs | |
+++ b/lapce-ui/src/picker.rs | |
@@ -216,10 +216,8 @@ impl Widget<LapceTabData> for FilePickerPwd { | |
ctx.set_handled(); | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
- ctx.request_paint(); | |
} else { | |
ctx.clear_cursor(); | |
- ctx.request_paint(); | |
} | |
} | |
Event::MouseDown(mouse_event) => { | |
@@ -637,10 +635,7 @@ pub fn paint_file_node_item_by_index( | |
.unwrap(); | |
ctx.draw_text( | |
&text_layout, | |
- Point::new( | |
- 38.0 + padding, | |
- y + (line_height - text_layout.size().height) / 2.0, | |
- ), | |
+ Point::new(38.0 + padding, y + text_layout.y_offset(line_height)), | |
); | |
} | |
let mut i = current; | |
@@ -757,10 +752,8 @@ impl Widget<LapceTabData> for FilePickerControl { | |
ctx.set_handled(); | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
- ctx.request_paint(); | |
} else { | |
ctx.clear_cursor(); | |
- ctx.request_paint(); | |
} | |
} | |
Event::MouseDown(mouse_event) => { | |
@@ -882,7 +875,7 @@ impl Widget<LapceTabData> for FilePickerControl { | |
let text_size = btn.text_layout.size(); | |
let btn_size = btn.rect.size(); | |
let x = btn.rect.x0 + (btn_size.width - text_size.width) / 2.0; | |
- let y = btn.rect.y0 + (btn_size.height - text_size.height) / 2.0; | |
+ let y = btn.rect.y0 + btn.text_layout.y_offset(btn_size.height); | |
ctx.draw_text(&btn.text_layout, Point::new(x, y)); | |
} | |
} | |
diff --git a/lapce-ui/src/plugin.rs b/lapce-ui/src/plugin.rs | |
index 887f9a968..45dde19c8 100644 | |
--- a/lapce-ui/src/plugin.rs | |
+++ b/lapce-ui/src/plugin.rs | |
@@ -78,7 +78,7 @@ impl Plugin { | |
.unwrap(); | |
ctx.draw_text( | |
&text_layout, | |
- Point::new(x, y + (self.line_height - text_layout.size().height) / 2.0), | |
+ Point::new(x, y + text_layout.y_offset(self.line_height)), | |
); | |
let text_layout = ctx | |
.text() | |
@@ -120,8 +120,7 @@ impl Plugin { | |
&text_layout, | |
Point::new( | |
x, | |
- y + self.line_height | |
- + (self.line_height - text_layout.size().height) / 2.0, | |
+ y + self.line_height + text_layout.y_offset(self.line_height), | |
), | |
); | |
} else { | |
@@ -129,8 +128,7 @@ impl Plugin { | |
&text_layout, | |
Point::new( | |
x, | |
- y + self.line_height | |
- + (self.line_height - text_layout.size().height) / 2.0, | |
+ y + self.line_height + text_layout.y_offset(self.line_height), | |
), | |
); | |
} | |
@@ -150,8 +148,7 @@ impl Plugin { | |
&text_layout, | |
Point::new( | |
x, | |
- y + self.line_height * 2.0 | |
- + (self.line_height - text_layout.size().height) / 2.0, | |
+ y + self.line_height * 2.0 + text_layout.y_offset(self.line_height), | |
), | |
); | |
@@ -185,10 +182,7 @@ impl Plugin { | |
ctx.fill(rect, &color); | |
ctx.draw_text( | |
&text_layout, | |
- Point::new( | |
- x + text_padding, | |
- y + (self.line_height - text_layout.size().height) / 2.0, | |
- ), | |
+ Point::new(x + text_padding, y + text_layout.y_offset(self.line_height)), | |
); | |
rect | |
} | |
diff --git a/lapce-ui/src/problem.rs b/lapce-ui/src/problem.rs | |
index 80d559ba4..d8f8898ae 100644 | |
--- a/lapce-ui/src/problem.rs | |
+++ b/lapce-ui/src/problem.rs | |
@@ -270,8 +270,7 @@ impl Widget<LapceTabData> for ProblemContent { | |
&text_layout, | |
Point::new( | |
line_height, | |
- line_height * i as f64 | |
- + (line_height - text_layout.size().height) / 2.0, | |
+ line_height * i as f64 + text_layout.y_offset(line_height), | |
), | |
); | |
@@ -308,8 +307,7 @@ impl Widget<LapceTabData> for ProblemContent { | |
&text_layout, | |
Point::new( | |
x, | |
- line_height * i as f64 | |
- + (line_height - text_layout.size().height) / 2.0, | |
+ line_height * i as f64 + text_layout.y_offset(line_height), | |
), | |
); | |
} | |
@@ -379,7 +377,7 @@ impl Widget<LapceTabData> for ProblemContent { | |
Point::new( | |
2.0 * line_height, | |
line_height * i as f64 | |
- + (line_height - text_layout.size().height) / 2.0, | |
+ + text_layout.y_offset(line_height), | |
), | |
); | |
} | |
@@ -453,7 +451,7 @@ impl Widget<LapceTabData> for ProblemContent { | |
Point::new( | |
3.0 * line_height, | |
line_height * i as f64 | |
- + (line_height - text_layout.size().height) / 2.0, | |
+ + text_layout.y_offset(line_height), | |
), | |
); | |
for line in related.message.split('\n') { | |
@@ -478,8 +476,7 @@ impl Widget<LapceTabData> for ProblemContent { | |
Point::new( | |
3.0 * line_height, | |
line_height * i as f64 | |
- + (line_height - text_layout.size().height) | |
- / 2.0, | |
+ + text_layout.y_offset(line_height), | |
), | |
); | |
} | |
diff --git a/lapce-ui/src/search.rs b/lapce-ui/src/search.rs | |
index 1d230905c..6cefbad09 100644 | |
--- a/lapce-ui/src/search.rs | |
+++ b/lapce-ui/src/search.rs | |
@@ -232,7 +232,7 @@ impl Widget<LapceTabData> for SearchContent { | |
Point::new( | |
self.line_height, | |
self.line_height * i as f64 | |
- + (self.line_height - text_layout.size().height) / 2.0, | |
+ + text_layout.y_offset(self.line_height), | |
), | |
); | |
@@ -270,7 +270,7 @@ impl Widget<LapceTabData> for SearchContent { | |
Point::new( | |
x, | |
self.line_height * i as f64 | |
- + (self.line_height - text_layout.size().height) / 2.0, | |
+ + text_layout.y_offset(self.line_height), | |
), | |
); | |
} | |
@@ -309,8 +309,7 @@ impl Widget<LapceTabData> for SearchContent { | |
Point::new( | |
self.line_height, | |
self.line_height * i as f64 | |
- + (self.line_height - text_layout.size().height) | |
- / 2.0, | |
+ + text_layout.y_offset(self.line_height), | |
), | |
); | |
} | |
diff --git a/lapce-ui/src/settings.rs b/lapce-ui/src/settings.rs | |
index 9ad202c13..098de9538 100644 | |
--- a/lapce-ui/src/settings.rs | |
+++ b/lapce-ui/src/settings.rs | |
@@ -27,6 +27,7 @@ use lapce_data::{ | |
keypress::KeyPressFocus, | |
settings::{LapceSettingsFocusData, SettingsValueKind}, | |
}; | |
+use serde::Serialize; | |
use xi_rope::Rope; | |
use crate::{ | |
@@ -58,6 +59,7 @@ impl LapceSettingsPanel { | |
data: &LapceTabData, | |
widget_id: WidgetId, | |
editor_tab_id: WidgetId, | |
+ keymap_input_view_id: WidgetId, | |
) -> Self { | |
let children = vec![ | |
WidgetPod::new( | |
@@ -73,7 +75,7 @@ impl LapceSettingsPanel { | |
LapceSettings::new_split(LapceSettingsKind::Terminal, data).boxed(), | |
), | |
WidgetPod::new(ThemeSettings::new_boxed().boxed()), | |
- WidgetPod::new(LapceKeymap::new_split(data).boxed()), | |
+ WidgetPod::new(LapceKeymap::new_split(keymap_input_view_id).boxed()), | |
]; | |
Self { | |
widget_id, | |
@@ -121,7 +123,7 @@ impl LapceSettingsPanel { | |
} | |
data.main_split.active_tab = Arc::new(Some(self.editor_tab_id)); | |
- data.focus = self.widget_id; | |
+ data.focus = Arc::new(self.widget_id); | |
data.focus_area = FocusArea::Editor; | |
ctx.request_focus(); | |
} | |
@@ -147,6 +149,7 @@ impl Widget<LapceTabData> for LapceSettingsPanel { | |
widget_id: self.widget_id, | |
editor_tab_id: self.editor_tab_id, | |
main_split: data.main_split.clone(), | |
+ config: data.config.clone(), | |
}; | |
let mut_keypress = Arc::make_mut(&mut keypress); | |
let performed_action = | |
@@ -167,6 +170,7 @@ impl Widget<LapceTabData> for LapceSettingsPanel { | |
widget_id: self.widget_id, | |
editor_tab_id: self.editor_tab_id, | |
main_split: data.main_split.clone(), | |
+ config: data.config.clone(), | |
}; | |
if focus.run_command(ctx, cmd, None, Modifiers::empty(), env) | |
== CommandExecuted::Yes | |
@@ -315,15 +319,13 @@ impl Widget<LapceTabData> for LapceSettingsPanel { | |
) | |
.build() | |
.unwrap(); | |
- let text_size = text_layout.size(); | |
ctx.draw_text( | |
&text_layout, | |
self.switcher_rect.origin() | |
+ ( | |
20.0, | |
i as f64 * self.switcher_line_height | |
- + (self.switcher_line_height / 2.0 | |
- - text_size.height / 2.0), | |
+ + text_layout.y_offset(self.switcher_line_height), | |
), | |
); | |
} | |
@@ -377,74 +379,56 @@ impl LapceSettings { | |
} | |
fn update_children(&mut self, ctx: &mut EventCtx, data: &mut LapceTabData) { | |
+ fn into_settings_map( | |
+ data: &impl Serialize, | |
+ ) -> HashMap<String, serde_json::Value> { | |
+ serde_json::to_value(data) | |
+ .and_then(serde_json::from_value) | |
+ .unwrap() | |
+ } | |
+ | |
self.children.clear(); | |
- let (kind, fields, descs, settings) = match self.kind { | |
- LapceSettingsKind::Core => { | |
- let settings: HashMap<String, serde_json::Value> = | |
- serde_json::from_value( | |
- serde_json::to_value(&data.config.lapce).unwrap(), | |
- ) | |
- .unwrap(); | |
- ( | |
- "lapce".to_string(), | |
- LapceConfig::FIELDS.to_vec(), | |
- LapceConfig::DESCS.to_vec(), | |
- settings, | |
- ) | |
- } | |
- LapceSettingsKind::UI => { | |
- let settings: HashMap<String, serde_json::Value> = | |
- serde_json::from_value( | |
- serde_json::to_value(&data.config.ui).unwrap(), | |
- ) | |
- .unwrap(); | |
- ( | |
- "ui".to_string(), | |
- UIConfig::FIELDS.to_vec(), | |
- UIConfig::DESCS.to_vec(), | |
- settings, | |
- ) | |
- } | |
- LapceSettingsKind::Editor => { | |
- let settings: HashMap<String, serde_json::Value> = | |
- serde_json::from_value( | |
- serde_json::to_value(&data.config.editor).unwrap(), | |
- ) | |
- .unwrap(); | |
- ( | |
- "editor".to_string(), | |
- EditorConfig::FIELDS.to_vec(), | |
- EditorConfig::DESCS.to_vec(), | |
- settings, | |
- ) | |
- } | |
- LapceSettingsKind::Terminal => { | |
- let settings: HashMap<String, serde_json::Value> = | |
- serde_json::from_value( | |
- serde_json::to_value(&data.config.terminal).unwrap(), | |
- ) | |
- .unwrap(); | |
- ( | |
- "terminal".to_string(), | |
- TerminalConfig::FIELDS.to_vec(), | |
- TerminalConfig::DESCS.to_vec(), | |
- settings, | |
- ) | |
- } | |
+ let (kind, fields, descs, mut settings) = match self.kind { | |
+ LapceSettingsKind::Core => ( | |
+ "lapce", | |
+ &LapceConfig::FIELDS[..], | |
+ &LapceConfig::DESCS[..], | |
+ into_settings_map(&data.config.lapce), | |
+ ), | |
+ LapceSettingsKind::UI => ( | |
+ "ui", | |
+ &UIConfig::FIELDS[..], | |
+ &UIConfig::DESCS[..], | |
+ into_settings_map(&data.config.ui), | |
+ ), | |
+ LapceSettingsKind::Editor => ( | |
+ "editor", | |
+ &EditorConfig::FIELDS[..], | |
+ &EditorConfig::DESCS[..], | |
+ into_settings_map(&data.config.editor), | |
+ ), | |
+ LapceSettingsKind::Terminal => ( | |
+ "terminal", | |
+ &TerminalConfig::FIELDS[..], | |
+ &TerminalConfig::DESCS[..], | |
+ into_settings_map(&data.config.terminal), | |
+ ), | |
}; | |
- for (i, field) in fields.into_iter().enumerate() { | |
+ for (field, desc) in fields.iter().zip(descs.iter()) { | |
+ // TODO(dbuga): we should generate kebab-case field names | |
let field = field.replace('_', "-"); | |
+ let value = settings.remove(&field).unwrap(); | |
self.children.push(WidgetPod::new( | |
LapcePadding::new( | |
(10.0, 10.0), | |
LapceSettingsItem::new( | |
data, | |
- kind.clone(), | |
- field.clone(), | |
- descs[i].to_string(), | |
- settings.get(&field).unwrap().clone(), | |
+ kind.to_string(), | |
+ field, | |
+ desc.to_string(), | |
+ value, | |
ctx.get_external_handle(), | |
), | |
) | |
@@ -553,7 +537,6 @@ struct LapceSettingsItem { | |
name_text: Option<PietTextLayout>, | |
desc_text: Option<PietTextLayout>, | |
value_text: Option<Option<PietTextLayout>>, | |
- input_rect: Rect, | |
input_widget: Option<WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>>, | |
} | |
@@ -626,7 +609,6 @@ impl LapceSettingsItem { | |
name_text: None, | |
desc_text: None, | |
value_text: None, | |
- input_rect: Rect::ZERO, | |
input_widget, | |
} | |
} | |
@@ -852,15 +834,8 @@ impl Widget<LapceTabData> for LapceSettingsItem { | |
} | |
} | |
} | |
- Event::MouseMove(mouse_event) => { | |
+ Event::MouseMove(_) => { | |
ctx.set_handled(); | |
- if self.input_rect.contains(mouse_event.pos) { | |
- ctx.set_cursor(&druid::Cursor::IBeam); | |
- ctx.request_paint(); | |
- } else { | |
- ctx.clear_cursor(); | |
- ctx.request_paint(); | |
- } | |
} | |
Event::Timer(token) | |
if self.value_changed && *token == self.last_idle_timer => | |
@@ -888,6 +863,9 @@ impl Widget<LapceTabData> for LapceSettingsItem { | |
data: &LapceTabData, | |
env: &Env, | |
) { | |
+ if let LifeCycle::HotChanged(_) = event { | |
+ ctx.request_paint(); | |
+ } | |
if let Some(input) = self.input_widget.as_mut() { | |
input.lifecycle(ctx, event, data, env); | |
} | |
@@ -1387,10 +1365,7 @@ impl Widget<LapceTabData> for ThemeSettings { | |
) | |
.build() | |
.unwrap(); | |
- ctx.draw_text( | |
- &header_text, | |
- Point::new(0.0, (30.0 - header_text.size().height) / 2.0), | |
- ); | |
+ ctx.draw_text(&header_text, Point::new(0.0, header_text.y_offset(30.0))); | |
for (i, input) in self.inputs.iter_mut().enumerate() { | |
let text_layout = &self.text_layouts.as_ref().unwrap()[i]; | |
@@ -1399,8 +1374,7 @@ impl Widget<LapceTabData> for ThemeSettings { | |
Point::new( | |
0.0, | |
input.layout_rect().y0 | |
- + (input.layout_rect().height() - text_layout.size().height) | |
- / 2.0, | |
+ + text_layout.y_offset(input.layout_rect().height()), | |
), | |
); | |
input.paint(ctx, data, env); | |
@@ -1420,7 +1394,6 @@ impl Widget<LapceTabData> for ThemeSettings { | |
) | |
.build() | |
.unwrap(); | |
- let reset_size = reset_text.size(); | |
for (_, _, rect) in self.changed_rects.iter() { | |
ctx.stroke( | |
rect.inflate(-0.5, -0.5), | |
@@ -1431,7 +1404,7 @@ impl Widget<LapceTabData> for ThemeSettings { | |
&reset_text, | |
Point::new( | |
rect.x0 + 10.0, | |
- rect.y0 + (rect.height() - reset_size.height) / 2.0, | |
+ rect.y0 + reset_text.y_offset(rect.height()), | |
), | |
) | |
} | |
diff --git a/lapce-ui/src/source_control.rs b/lapce-ui/src/source_control.rs | |
index 27d6f5c48..226edf9be 100644 | |
--- a/lapce-ui/src/source_control.rs | |
+++ b/lapce-ui/src/source_control.rs | |
@@ -104,7 +104,7 @@ impl SourceControlFileList { | |
let source_control = Arc::make_mut(&mut data.source_control); | |
source_control.active = self.widget_id; | |
data.focus_area = FocusArea::Panel(PanelKind::SourceControl); | |
- data.focus = self.widget_id; | |
+ data.focus = Arc::new(self.widget_id); | |
} | |
} | |
@@ -317,7 +317,7 @@ impl Widget<LapceTabData> for SourceControlFileList { | |
&text_layout, | |
Point::new( | |
self.line_height * 2.0, | |
- y + (self.line_height - text_layout.size().height) / 2.0, | |
+ y + text_layout.y_offset(self.line_height), | |
), | |
); | |
let folder = path | |
@@ -346,7 +346,7 @@ impl Widget<LapceTabData> for SourceControlFileList { | |
&text_layout, | |
Point::new( | |
self.line_height * 2.0 + x + 5.0, | |
- y + (self.line_height - text_layout.size().height) / 2.0, | |
+ y + text_layout.y_offset(self.line_height), | |
), | |
); | |
} | |
diff --git a/lapce-ui/src/split.rs b/lapce-ui/src/split.rs | |
index 86d74b92b..14811b640 100644 | |
--- a/lapce-ui/src/split.rs | |
+++ b/lapce-ui/src/split.rs | |
@@ -67,11 +67,16 @@ pub fn split_content_widget( | |
.boxed(); | |
editor_tab = editor_tab.with_child(editor); | |
} | |
- EditorTabChild::Settings(widget_id, editor_tab_id) => { | |
+ EditorTabChild::Settings { | |
+ settings_widget_id, | |
+ editor_tab_id, | |
+ keymap_input_view_id, | |
+ } => { | |
let settings = LapceSettingsPanel::new( | |
data, | |
- *widget_id, | |
+ *settings_widget_id, | |
*editor_tab_id, | |
+ *keymap_input_view_id, | |
) | |
.boxed(); | |
editor_tab = editor_tab.with_child(settings); | |
@@ -1166,7 +1171,7 @@ impl Widget<LapceTabData> for LapceSplit { | |
)); | |
} else { | |
ctx.request_focus(); | |
- data.focus = self.split_id; | |
+ data.focus = Arc::new(self.split_id); | |
data.focus_area = FocusArea::Editor; | |
} | |
} | |
diff --git a/lapce-ui/src/status.rs b/lapce-ui/src/status.rs | |
index d0f461ee7..4aea7c6bf 100644 | |
--- a/lapce-ui/src/status.rs | |
+++ b/lapce-ui/src/status.rs | |
@@ -1,8 +1,8 @@ | |
use druid::{ | |
kurbo::Line, | |
piet::{PietTextLayout, Svg, Text, TextLayout, TextLayoutBuilder}, | |
- Command, Event, EventCtx, MouseEvent, PaintCtx, Point, Rect, RenderContext, | |
- Size, Target, Widget, | |
+ Command, Data, Event, EventCtx, MouseEvent, PaintCtx, Point, Rect, | |
+ RenderContext, Size, Target, Widget, | |
}; | |
use lapce_core::mode::Mode; | |
use lapce_data::{ | |
@@ -19,6 +19,7 @@ pub struct LapceStatus { | |
clickable_items: Vec<(Rect, Command)>, | |
mouse_pos: Point, | |
icon_size: f64, | |
+ active_icon: Option<Rect>, | |
} | |
impl LapceStatus { | |
@@ -28,6 +29,7 @@ impl LapceStatus { | |
clickable_items: Vec::new(), | |
mouse_pos: Point::ZERO, | |
icon_size: 13.0, | |
+ active_icon: None, | |
} | |
} | |
@@ -92,14 +94,16 @@ impl LapceStatus { | |
icons | |
} | |
- fn icon_hit_test(&self, mouse_event: &MouseEvent) -> bool { | |
+ fn icon_hit_test(&mut self, mouse_event: &MouseEvent) -> bool { | |
for icon in self.panel_icons.iter() { | |
if icon.rect.contains(mouse_event.pos) { | |
+ self.active_icon = Some(icon.rect); | |
return true; | |
} | |
} | |
for (rect, _) in self.clickable_items.iter() { | |
if rect.contains(mouse_event.pos) { | |
+ self.active_icon = Some(*rect); | |
return true; | |
} | |
} | |
@@ -156,7 +160,7 @@ impl LapceStatus { | |
None | |
}; | |
- let point = Point::new(left, (height - text_layout.size().height) / 2.0); | |
+ let point = Point::new(left, text_layout.y_offset(height)); | |
(left + text_layout.size().width, svg, (point, text_layout)) | |
} | |
@@ -197,7 +201,7 @@ impl LapceStatus { | |
let point = Point::new( | |
right - text_layout.size().width, | |
- (height - text_layout.size().height) / 2.0, | |
+ text_layout.y_offset(height), | |
); | |
(right - text_layout.size().width, svg, (point, text_layout)) | |
} | |
@@ -220,11 +224,14 @@ impl Widget<LapceTabData> for LapceStatus { | |
match event { | |
Event::MouseMove(mouse_event) => { | |
self.mouse_pos = mouse_event.pos; | |
+ let active_icon = self.active_icon; | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
- ctx.request_paint(); | |
} else { | |
+ self.active_icon = None; | |
ctx.clear_cursor(); | |
+ } | |
+ if active_icon != self.active_icon { | |
ctx.request_paint(); | |
} | |
} | |
@@ -273,7 +280,7 @@ impl Widget<LapceTabData> for LapceStatus { | |
return; | |
} | |
- if !old_data.progresses.ptr_eq(&data.progresses) { | |
+ if !old_data.progresses.same(&data.progresses) { | |
ctx.request_paint(); | |
} | |
} | |
@@ -358,7 +365,7 @@ impl Widget<LapceTabData> for LapceStatus { | |
ctx.fill(fill_size.to_rect(), data.config.get_color_unchecked(color)); | |
ctx.draw_text( | |
&text_layout, | |
- Point::new(5.0, (size.height - text_layout.size().height) / 2.0), | |
+ Point::new(5.0, text_layout.y_offset(size.height)), | |
); | |
left += text_size.width + 10.0; | |
} | |
@@ -452,10 +459,7 @@ impl Widget<LapceTabData> for LapceStatus { | |
.unwrap(); | |
ctx.draw_text( | |
&text_layout, | |
- Point::new( | |
- left + 10.0, | |
- (size.height - text_layout.size().height) / 2.0, | |
- ), | |
+ Point::new(left + 10.0, text_layout.y_offset(size.height)), | |
); | |
left += 10.0 + text_layout.size().width; | |
} | |
diff --git a/lapce-ui/src/tab.rs b/lapce-ui/src/tab.rs | |
index 6bf5b6063..b71ee0529 100644 | |
--- a/lapce-ui/src/tab.rs | |
+++ b/lapce-ui/src/tab.rs | |
@@ -48,9 +48,8 @@ use crate::{ | |
editor::view::LapceEditorView, explorer::FileExplorer, hover::HoverContainer, | |
panel::PanelContainer, picker::FilePicker, plugin::Plugin, | |
problem::new_problem_panel, search::new_search_panel, | |
- settings::LapceSettingsPanel, source_control::new_source_control_panel, | |
- split::split_data_widget, status::LapceStatus, svg::get_svg, | |
- terminal::TerminalPanel, title::Title, | |
+ source_control::new_source_control_panel, split::split_data_widget, | |
+ status::LapceStatus, svg::get_svg, terminal::TerminalPanel, title::Title, | |
}; | |
pub const LAPCE_TAB_META: Selector<SingleUse<LapceTabMeta>> = | |
@@ -82,7 +81,6 @@ pub struct LapceTab { | |
rename: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>, | |
status: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>, | |
picker: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>, | |
- settings: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>, | |
about: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>, | |
alert: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>, | |
panel_left: WidgetPod<LapceTabData, PanelContainer>, | |
@@ -91,6 +89,7 @@ pub struct LapceTab { | |
current_bar_hover: Option<PanelResizePosition>, | |
width: f64, | |
height: f64, | |
+ title_height: f64, | |
status_height: f64, | |
mouse_pos: Point, | |
} | |
@@ -115,9 +114,6 @@ impl LapceTab { | |
let status = LapceStatus::new(); | |
let picker = FilePicker::new(data); | |
- let settings = | |
- LapceSettingsPanel::new(data, WidgetId::next(), WidgetId::next()); | |
- | |
let about = AboutBox::new(data); | |
let alert = AlertBox::new(data); | |
@@ -188,7 +184,6 @@ impl LapceTab { | |
rename: WidgetPod::new(rename.boxed()), | |
picker: WidgetPod::new(picker.boxed()), | |
status: WidgetPod::new(status.boxed()), | |
- settings: WidgetPod::new(settings.boxed()), | |
about: WidgetPod::new(about.boxed()), | |
alert: WidgetPod::new(alert.boxed()), | |
panel_left: WidgetPod::new(panel_left), | |
@@ -198,6 +193,7 @@ impl LapceTab { | |
width: 0.0, | |
height: 0.0, | |
status_height: 0.0, | |
+ title_height: 0.0, | |
mouse_pos: Point::ZERO, | |
} | |
} | |
@@ -287,7 +283,19 @@ impl LapceTab { | |
PanelResizePosition::Bottom => { | |
let bottom = | |
self.height - mouse_pos.y.round() - self.status_height; | |
- Arc::make_mut(&mut data.panel).size.bottom = bottom.max(180.0); | |
+ | |
+ let header_height = data.config.ui.header_height() as f64; | |
+ // The maximum position (from the bottom) that the bottom split is allowed to reach | |
+ let minimum = self.height | |
+ - self.title_height | |
+ - self.status_height | |
+ - header_height | |
+ - 1.0; | |
+ | |
+ Arc::make_mut(&mut data.panel).size.bottom = | |
+ bottom.max(180.0).min(minimum); | |
+ | |
+ // Check if it should snap the bottom panel away, if you are too low | |
if bottom < 90.0 { | |
if data | |
.panel | |
@@ -548,12 +556,11 @@ impl LapceTab { | |
rect.y0 + (size.height - height) / 2.0, | |
)); | |
ctx.draw_svg(&tab_rect.svg, svg_rect, None); | |
- let text_size = tab_rect.text_layout.size(); | |
ctx.draw_text( | |
&tab_rect.text_layout, | |
Point::new( | |
rect.x0 + size.height, | |
- rect.y0 + (size.height - text_size.height) / 2.0, | |
+ rect.y0 + tab_rect.text_layout.y_offset(size.height), | |
), | |
); | |
} | |
@@ -699,7 +706,7 @@ impl LapceTab { | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::SetWorkspace(workspace), | |
- Target::Window(data.window_id), | |
+ Target::Window(*data.window_id), | |
)); | |
} | |
Event::Command(cmd) if cmd.is(LAPCE_OPEN_FILE) => { | |
@@ -1048,15 +1055,19 @@ impl LapceTab { | |
lsp_types::ProgressParamsValue::WorkDone(progress) => { | |
match progress { | |
lsp_types::WorkDoneProgress::Begin(begin) => { | |
- data.progresses.push_back(WorkProgress { | |
- token: params.token.clone(), | |
- title: begin.title.clone(), | |
- message: begin.message.clone(), | |
- percentage: begin.percentage, | |
- }); | |
+ Arc::make_mut(&mut data.progresses).push( | |
+ WorkProgress { | |
+ token: params.token.clone(), | |
+ title: begin.title.clone(), | |
+ message: begin.message.clone(), | |
+ percentage: begin.percentage, | |
+ }, | |
+ ); | |
} | |
lsp_types::WorkDoneProgress::Report(report) => { | |
- for p in data.progresses.iter_mut() { | |
+ for p in Arc::make_mut(&mut data.progresses) | |
+ .iter_mut() | |
+ { | |
if p.token == params.token { | |
p.message = report.message.clone(); | |
p.percentage = report.percentage; | |
@@ -1077,7 +1088,8 @@ impl LapceTab { | |
.sorted() | |
.rev() | |
{ | |
- data.progresses.remove(i); | |
+ Arc::make_mut(&mut data.progresses) | |
+ .remove(i); | |
} | |
} | |
} | |
@@ -1564,7 +1576,7 @@ impl LapceTab { | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.focus), | |
+ Target::Widget(*data.focus), | |
)); | |
ctx.set_handled(); | |
} | |
@@ -1937,7 +1949,6 @@ impl Widget<LapceTabData> for LapceTab { | |
self.hover.lifecycle(ctx, event, data, env); | |
self.rename.lifecycle(ctx, event, data, env); | |
self.picker.lifecycle(ctx, event, data, env); | |
- self.settings.lifecycle(ctx, event, data, env); | |
self.about.lifecycle(ctx, event, data, env); | |
self.alert.lifecycle(ctx, event, data, env); | |
self.panel_left.lifecycle(ctx, event, data, env); | |
@@ -1964,14 +1975,14 @@ impl Widget<LapceTabData> for LapceTab { | |
ctx.request_paint(); | |
} | |
- if !old_data.about.active != data.about.active { | |
+ if old_data.about.active != data.about.active { | |
ctx.request_layout(); | |
} | |
- if !old_data.alert.active != data.alert.active { | |
+ if old_data.alert.active != data.alert.active { | |
ctx.request_layout(); | |
} | |
- if old_data | |
+ if !old_data | |
.main_split | |
.diagnostics | |
.same(&data.main_split.diagnostics) | |
@@ -2014,7 +2025,6 @@ impl Widget<LapceTabData> for LapceTab { | |
self.rename.update(ctx, data, env); | |
self.status.update(ctx, data, env); | |
self.picker.update(ctx, data, env); | |
- self.settings.update(ctx, data, env); | |
self.about.update(ctx, data, env); | |
self.alert.update(ctx, data, env); | |
self.panel_left.update(ctx, data, env); | |
@@ -2036,7 +2046,8 @@ impl Widget<LapceTabData> for LapceTab { | |
self.title.layout(ctx, bc, data, env); | |
self.title.set_origin(ctx, data, env, Point::ZERO); | |
- let title_height = 36.0; | |
+ self.title_height = 36.0; | |
+ let title_height = self.title_height; | |
let status_size = self.status.layout(ctx, bc, data, env); | |
self.status.set_origin( | |
@@ -2302,7 +2313,6 @@ impl Widget<LapceTabData> for LapceTab { | |
self.completion.paint(ctx, data, env); | |
self.hover.paint(ctx, data, env); | |
self.picker.paint(ctx, data, env); | |
- self.settings.paint(ctx, data, env); | |
ctx.incr_alpha_depth(); | |
self.paint_drag_on_panel(ctx, data); | |
self.paint_drag(ctx, data); | |
@@ -2391,7 +2401,7 @@ impl Widget<LapceTabData> for LapceTabHeader { | |
Event::MouseUp(mouse_event) => { | |
if mouse_event.button.is_right() { | |
let tab_id = data.id; | |
- let window_id = data.window_id; | |
+ let window_id = *data.window_id; | |
let mut menu = druid::Menu::<LapceData>::new("Tab"); | |
let item = druid::MenuItem::new("Move Tab To a New Window") | |
@@ -2514,7 +2524,7 @@ impl Widget<LapceTabData> for LapceTabHeader { | |
let size = ctx.size(); | |
let text_size = text_layout.size(); | |
let x = (size.width - text_size.width) / 2.0; | |
- let y = (size.height - text_size.height) / 2.0; | |
+ let y = text_layout.y_offset(size.height); | |
ctx.draw_text(&text_layout, Point::new(x, y)); | |
if ctx.is_hot() { | |
diff --git a/lapce-ui/src/terminal.rs b/lapce-ui/src/terminal.rs | |
index a2caeb812..c699f610c 100644 | |
--- a/lapce-ui/src/terminal.rs | |
+++ b/lapce-ui/src/terminal.rs | |
@@ -6,7 +6,7 @@ use alacritty_terminal::{ | |
term::{cell::Flags, search::RegexSearch}, | |
}; | |
use druid::{ | |
- piet::{Text, TextAttribute, TextLayout, TextLayoutBuilder}, | |
+ piet::{Text, TextAttribute, TextLayoutBuilder}, | |
BoxConstraints, Command, Data, Env, Event, EventCtx, FontWeight, LayoutCtx, | |
LifeCycle, LifeCycleCtx, MouseEvent, PaintCtx, Point, Rect, RenderContext, Size, | |
Target, UpdateCtx, Widget, WidgetExt, WidgetId, WidgetPod, | |
@@ -262,6 +262,7 @@ struct LapceTerminalHeader { | |
icons: Vec<LapceIcon>, | |
mouse_pos: Point, | |
view_is_hot: bool, | |
+ hover_rect: Option<Rect>, | |
} | |
impl LapceTerminalHeader { | |
@@ -274,6 +275,7 @@ impl LapceTerminalHeader { | |
icon_padding: 4.0, | |
icons: Vec::new(), | |
view_is_hot: false, | |
+ hover_rect: None, | |
} | |
} | |
@@ -316,9 +318,10 @@ impl LapceTerminalHeader { | |
icons | |
} | |
- fn icon_hit_test(&self, mouse_event: &MouseEvent) -> bool { | |
+ fn icon_hit_test(&mut self, mouse_event: &MouseEvent) -> bool { | |
for icon in self.icons.iter() { | |
if icon.rect.contains(mouse_event.pos) { | |
+ self.hover_rect = Some(icon.rect); | |
return true; | |
} | |
} | |
@@ -345,11 +348,14 @@ impl Widget<LapceTabData> for LapceTerminalHeader { | |
match event { | |
Event::MouseMove(mouse_event) => { | |
self.mouse_pos = mouse_event.pos; | |
+ let hover_rect = self.hover_rect; | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
- ctx.request_paint(); | |
} else { | |
+ self.hover_rect = None; | |
ctx.clear_cursor(); | |
+ } | |
+ if hover_rect != self.hover_rect { | |
ctx.request_paint(); | |
} | |
} | |
@@ -431,7 +437,7 @@ impl Widget<LapceTabData> for LapceTerminalHeader { | |
) | |
.build() | |
.unwrap(); | |
- let y = (self.height - text_layout.size().height) / 2.0; | |
+ let y = text_layout.y_offset(self.height); | |
ctx.draw_text(&text_layout, Point::new(self.height, y)); | |
}); | |
@@ -480,7 +486,7 @@ impl LapceTerminal { | |
ctx.request_focus(); | |
Arc::make_mut(&mut data.terminal).active = self.widget_id; | |
Arc::make_mut(&mut data.terminal).active_term_id = self.term_id; | |
- data.focus = self.widget_id; | |
+ data.focus = Arc::new(self.widget_id); | |
data.focus_area = FocusArea::Panel(PanelKind::Terminal); | |
if let Some((index, position)) = | |
data.panel.panel_position(&PanelKind::Terminal) | |
@@ -607,7 +613,6 @@ impl Widget<LapceTabData> for LapceTerminal { | |
let char_size = data.config.editor_text_size(ctx.text(), "W"); | |
let char_width = char_size.width; | |
let line_height = data.config.terminal_line_height() as f64; | |
- let y_shift = (line_height - char_size.height) / 2.0; | |
let terminal = data.terminal.terminals.get(&self.term_id).unwrap(); | |
let raw = terminal.raw.lock(); | |
@@ -748,7 +753,10 @@ impl Widget<LapceTabData> for LapceTerminal { | |
.default_attribute(TextAttribute::Weight(FontWeight::BOLD)); | |
} | |
let text_layout = builder.build().unwrap(); | |
- ctx.draw_text(&text_layout, Point::new(x, y + y_shift)); | |
+ ctx.draw_text( | |
+ &text_layout, | |
+ Point::new(x, y + text_layout.y_offset(line_height)), | |
+ ); | |
} | |
} | |
if data.find.visual { | |
diff --git a/lapce-ui/src/title.rs b/lapce-ui/src/title.rs | |
index 6cc82c3a4..a904250fb 100644 | |
--- a/lapce-ui/src/title.rs | |
+++ b/lapce-ui/src/title.rs | |
@@ -40,6 +40,7 @@ pub struct Title { | |
circles: Vec<(Circle, Color)>, | |
palette: WidgetPod<LapceTabData, Box<dyn Widget<LapceTabData>>>, | |
dragable_area: Region, | |
+ hover_rect: Option<Rect>, | |
} | |
impl Title { | |
@@ -57,6 +58,7 @@ impl Title { | |
circles: Vec::new(), | |
palette: WidgetPod::new(palette.boxed()), | |
dragable_area: Region::EMPTY, | |
+ hover_rect: None, | |
} | |
} | |
@@ -431,9 +433,9 @@ impl Title { | |
MenuKind::Item(MenuItem { | |
desc: Some( | |
if latest_version.is_some() && latest_version != Some(*VERSION) { | |
- format!("Restart to Update ({})", latest_version.unwrap()) | |
+ format!("Restart to update ({})", latest_version.unwrap()) | |
} else { | |
- "Restart to Update".to_string() | |
+ "No update available".to_string() | |
}, | |
), | |
command: LapceCommand { | |
@@ -500,7 +502,7 @@ impl Title { | |
if !data.multiple_tab && data.config.lapce.custom_titlebar { | |
x += size.height; | |
let (window_controls, svgs) = window_controls( | |
- data.window_id, | |
+ *data.window_id, | |
window_state, | |
x, | |
size.height, | |
@@ -643,14 +645,16 @@ impl Title { | |
)); | |
} | |
- fn icon_hit_test(&self, mouse_event: &MouseEvent) -> bool { | |
+ fn icon_hit_test(&mut self, mouse_event: &MouseEvent) -> bool { | |
for (rect, _) in self.menus.iter() { | |
if rect.contains(mouse_event.pos) { | |
+ self.hover_rect = Some(*rect); | |
return true; | |
} | |
} | |
for (rect, _) in self.window_controls.iter() { | |
if rect.contains(mouse_event.pos) { | |
+ self.hover_rect = Some(*rect); | |
return true; | |
} | |
} | |
@@ -703,13 +707,13 @@ impl Widget<LapceTabData> for Title { | |
} | |
Event::MouseMove(mouse_event) => { | |
self.mouse_pos = mouse_event.pos; | |
+ let hover_rect = self.hover_rect; | |
if self.icon_hit_test(mouse_event) { | |
ctx.set_cursor(&druid::Cursor::Pointer); | |
- ctx.request_paint(); | |
ctx.set_handled(); | |
} else { | |
+ self.hover_rect = None; | |
ctx.clear_cursor(); | |
- ctx.request_paint(); | |
#[cfg(target_os = "windows")] | |
// ! Currently implemented on Windows only | |
@@ -723,6 +727,9 @@ impl Widget<LapceTabData> for Title { | |
ctx.window().handle_titlebar(true); | |
} | |
} | |
+ if hover_rect != self.hover_rect { | |
+ ctx.request_paint(); | |
+ } | |
} | |
Event::MouseDown(mouse_event) => { | |
if mouse_event.button.is_left() { | |
@@ -747,7 +754,7 @@ impl Widget<LapceTabData> for Title { | |
ctx.submit_command( | |
druid::commands::CONFIGURE_WINDOW | |
.with(WindowConfig::default().set_window_state(state)) | |
- .to(Target::Window(data.window_id)), | |
+ .to(Target::Window(*data.window_id)), | |
); | |
return; | |
} | |
@@ -776,10 +783,20 @@ impl Widget<LapceTabData> for Title { | |
fn update( | |
&mut self, | |
ctx: &mut druid::UpdateCtx, | |
- _old_data: &LapceTabData, | |
+ old_data: &LapceTabData, | |
data: &LapceTabData, | |
env: &Env, | |
) { | |
+ if data.main_split.can_jump_location_forward() | |
+ != old_data.main_split.can_jump_location_forward() | |
+ { | |
+ ctx.request_layout(); | |
+ } | |
+ if data.main_split.can_jump_location_backward() | |
+ != old_data.main_split.can_jump_location_backward() | |
+ { | |
+ ctx.request_layout(); | |
+ } | |
self.palette.update(ctx, data, env); | |
} | |
@@ -828,43 +845,43 @@ impl Widget<LapceTabData> for Title { | |
let arrow_left_rect = Size::new(36.0, 36.0) | |
.to_rect() | |
.with_origin(Point::new(palette_origin.x - 36.0 - 36.0, 0.0)); | |
- let (arrow_left_svg_color, arrow_left_svg_hover_color) = | |
- if data.main_split.current_location < 1 { | |
- ( | |
- Some( | |
- data.config | |
- .get_color_unchecked(LapceTheme::EDITOR_DIM) | |
- .clone(), | |
- ), | |
- None, | |
- ) | |
- } else { | |
- self.menus.push(( | |
- arrow_left_rect, | |
- Command::new( | |
- LAPCE_COMMAND, | |
- LapceCommand { | |
- kind: CommandKind::Focus( | |
- FocusCommand::JumpLocationBackward, | |
- ), | |
- data: None, | |
- }, | |
- target, | |
- ), | |
- )); | |
- ( | |
- Some( | |
- data.config | |
- .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) | |
- .clone(), | |
- ), | |
- Some( | |
- data.config | |
- .get_color_unchecked(LapceTheme::PANEL_CURRENT) | |
- .clone(), | |
- ), | |
- ) | |
- }; | |
+ let (arrow_left_svg_color, arrow_left_svg_hover_color) = if !data | |
+ .main_split | |
+ .can_jump_location_backward() | |
+ { | |
+ ( | |
+ Some( | |
+ data.config | |
+ .get_color_unchecked(LapceTheme::EDITOR_DIM) | |
+ .clone(), | |
+ ), | |
+ None, | |
+ ) | |
+ } else { | |
+ self.menus.push(( | |
+ arrow_left_rect, | |
+ Command::new( | |
+ LAPCE_COMMAND, | |
+ LapceCommand { | |
+ kind: CommandKind::Focus(FocusCommand::JumpLocationBackward), | |
+ data: None, | |
+ }, | |
+ target, | |
+ ), | |
+ )); | |
+ ( | |
+ Some( | |
+ data.config | |
+ .get_color_unchecked(LapceTheme::EDITOR_FOREGROUND) | |
+ .clone(), | |
+ ), | |
+ Some( | |
+ data.config | |
+ .get_color_unchecked(LapceTheme::PANEL_CURRENT) | |
+ .clone(), | |
+ ), | |
+ ) | |
+ }; | |
self.svgs.push(( | |
get_svg("arrow-left.svg").unwrap(), | |
arrow_left_rect.inflate(-10.5, -10.5), | |
@@ -875,12 +892,9 @@ impl Widget<LapceTabData> for Title { | |
let arrow_right_rect = Size::new(36.0, 36.0) | |
.to_rect() | |
.with_origin(Point::new(palette_origin.x - 36.0, 0.0)); | |
- let (arrow_right_svg_color, arrow_right_svg_hover_color) = if data | |
+ let (arrow_right_svg_color, arrow_right_svg_hover_color) = if !data | |
.main_split | |
- .locations | |
- .is_empty() | |
- || data.main_split.current_location | |
- >= data.main_split.locations.len() - 1 | |
+ .can_jump_location_forward() | |
{ | |
( | |
Some( | |
diff --git a/lapce-ui/src/window.rs b/lapce-ui/src/window.rs | |
index 4a90c465b..8c022fbdd 100644 | |
--- a/lapce-ui/src/window.rs | |
+++ b/lapce-ui/src/window.rs | |
@@ -95,19 +95,19 @@ impl LapceWindow { | |
if let Some(tab) = data.tabs.remove(&data.active_id) { | |
tab.proxy.stop(); | |
} | |
- data.active_id = tab_id; | |
+ data.active_id = Arc::new(tab_id); | |
} else { | |
self.tabs | |
.insert(data.active + 1, WidgetPod::new(tab.boxed())); | |
self.tab_headers | |
.insert(data.active + 1, WidgetPod::new(tab_header)); | |
data.active += 1; | |
- data.active_id = tab_id; | |
+ data.active_id = Arc::new(tab_id); | |
} | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.active_id), | |
+ Target::Widget(*data.active_id), | |
)); | |
data.tabs_order = Arc::new(self.tabs.iter().map(|t| t.id()).collect()); | |
let _ = data.db.save_tabs_async(data); | |
@@ -148,11 +148,11 @@ impl LapceWindow { | |
if data.active >= self.tabs.len() { | |
data.active = self.tabs.len() - 1; | |
} | |
- data.active_id = self.tabs[data.active].id(); | |
+ data.active_id = Arc::new(self.tabs[data.active].id()); | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.active_id), | |
+ Target::Widget(*data.active_id), | |
)); | |
} | |
_ => (), | |
@@ -212,7 +212,7 @@ impl Widget<LapceWindowData> for LapceWindow { | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.active_id), | |
+ Target::Widget(*data.active_id), | |
)); | |
} | |
Event::Internal(InternalEvent::MouseLeave) => { | |
@@ -312,7 +312,7 @@ impl Widget<LapceWindowData> for LapceWindow { | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.active_id), | |
+ Target::Widget(*data.active_id), | |
)); | |
ctx.set_handled(); | |
} | |
@@ -412,12 +412,12 @@ impl Widget<LapceWindowData> for LapceWindow { | |
if tab_id == &tab.id() { | |
if i != data.active { | |
data.active = i; | |
- data.active_id = tab.id(); | |
+ data.active_id = Arc::new(tab.id()); | |
let _ = data.db.save_tabs_async(data); | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.active_id), | |
+ Target::Widget(*data.active_id), | |
)); | |
} | |
return; | |
@@ -444,12 +444,12 @@ impl Widget<LapceWindowData> for LapceWindow { | |
data.tabs.get(&data.active_id).unwrap(), | |
); | |
data.active = new_index; | |
- data.active_id = self.tabs[new_index].id(); | |
+ data.active_id = Arc::new(self.tabs[new_index].id()); | |
let _ = data.db.save_tabs_async(data); | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.active_id), | |
+ Target::Widget(*data.active_id), | |
)); | |
ctx.request_layout(); | |
ctx.set_handled(); | |
@@ -464,12 +464,12 @@ impl Widget<LapceWindowData> for LapceWindow { | |
data.tabs.get(&data.active_id).unwrap(), | |
); | |
data.active = new_index; | |
- data.active_id = self.tabs[new_index].id(); | |
+ data.active_id = Arc::new(self.tabs[new_index].id()); | |
let _ = data.db.save_tabs_async(data); | |
ctx.submit_command(Command::new( | |
LAPCE_UI_COMMAND, | |
LapceUICommand::Focus, | |
- Target::Widget(data.active_id), | |
+ Target::Widget(*data.active_id), | |
)); | |
ctx.request_layout(); | |
ctx.set_handled(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment