on master help sign in
significantly improve treesitter performance while editing large files (#4716)
f538b69Pascal Kuthe
Cargo.lock+25−2
@@ -13,6 +13,18 @@ dependencies = [
13 "version_check",
13 "version_check",
14]
14]
15
15
16+[[package]]
17+name = "ahash"
18+version = "0.8.2"
19+source = "registry+https://github.com/rust-lang/crates.io-index"
20+checksum = "bf6ccdb167abbf410dcb915cabd428929d7f6a04980b54a11f26a39f1c7f7107"
21+dependencies = [
22+ "cfg-if",
23+ "getrandom",
24+ "once_cell",
25+ "version_check",
26+]
27+
16[[package]]
28[[package]]
17name = "aho-corasick"
29name = "aho-corasick"
18version = "0.7.18"
30version = "0.7.18"
@@ -400,18 +412,29 @@ version = "0.12.3"
400source = "registry+https://github.com/rust-lang/crates.io-index"
412source = "registry+https://github.com/rust-lang/crates.io-index"
401checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
413checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
402dependencies = [
414dependencies = [
403− "ahash",
415+ "ahash 0.7.6",
416+]
417+
418+[[package]]
419+name = "hashbrown"
420+version = "0.13.1"
421+source = "registry+https://github.com/rust-lang/crates.io-index"
422+checksum = "33ff8ae62cd3a9102e5637afc8452c55acf3844001bd5374e0b0bd7b6616c038"
423+dependencies = [
424+ "ahash 0.8.2",
404]
425]
405
426
406[[package]]
427[[package]]
407name = "helix-core"
428name = "helix-core"
408version = "0.6.0"
429version = "0.6.0"
409dependencies = [
430dependencies = [
431+ "ahash 0.8.2",
410 "arc-swap",
432 "arc-swap",
411 "bitflags",
433 "bitflags",
412 "chrono",
434 "chrono",
413 "encoding_rs",
435 "encoding_rs",
414 "etcetera",
436 "etcetera",
437+ "hashbrown 0.13.1",
415 "helix-loader",
438 "helix-loader",
416 "log",
439 "log",
417 "once_cell",
440 "once_cell",
@@ -1288,7 +1311,7 @@ version = "0.1.4"
1288source = "registry+https://github.com/rust-lang/crates.io-index"
1311source = "registry+https://github.com/rust-lang/crates.io-index"
1289checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
1312checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
1290dependencies = [
1313dependencies = [
1291− "hashbrown",
1314+ "hashbrown 0.12.3",
1292 "regex",
1315 "regex",
1293]
1316]
1294
1317
helix-core/Cargo.toml+2
@@ -30,6 +30,8 @@ once_cell = "1.16"
30arc-swap = "1"
30arc-swap = "1"
31regex = "1"
31regex = "1"
32bitflags = "1.3"
32bitflags = "1.3"
33+ahash = "0.8.2"
34+hashbrown = { version = "0.13.1", features = ["raw"] }
33
35
34log = "0.4"
36log = "0.4"
35serde = { version = "1.0", features = ["derive"] }
37serde = { version = "1.0", features = ["derive"] }
helix-core/src/syntax.rs+79−38
@@ -7,8 +7,10 @@ use crate::{
7 Rope, RopeSlice, Tendril,
7 Rope, RopeSlice, Tendril,
8};
8};
9
9
10+use ahash::RandomState;
10use arc_swap::{ArcSwap, Guard};
11use arc_swap::{ArcSwap, Guard};
11use bitflags::bitflags;
12use bitflags::bitflags;
13+use hashbrown::raw::RawTable;
12use slotmap::{DefaultKey as LayerId, HopSlotMap};
14use slotmap::{DefaultKey as LayerId, HopSlotMap};
13
15
14use std::{
16use std::{
@@ -16,7 +18,8 @@ use std::{
16 cell::RefCell,
18 cell::RefCell,
17 collections::{HashMap, VecDeque},
19 collections::{HashMap, VecDeque},
18 fmt,
20 fmt,
21+ hash::{Hash, Hasher},
19− mem::replace,
22+ mem::{replace, transmute},
20 path::Path,
23 path::Path,
21 str::FromStr,
24 str::FromStr,
22 sync::Arc,
25 sync::Arc,
@@ -770,30 +773,38 @@ impl Syntax {
770 // Convert the changeset into tree sitter edits.
773 // Convert the changeset into tree sitter edits.
771 let edits = generate_edits(old_source, changeset);
774 let edits = generate_edits(old_source, changeset);
772
775
776+ // This table allows inverse indexing of `layers`.
777+ // That is by hashing a `Layer` you can find
778+ // the `LayerId` of an existing equivalent `Layer` in `layers`.
779+ //
780+ // It is used to determine if a new layer exists for an injection
781+ // or if an existing layer needs to be updated.
782+ let mut layers_table = RawTable::with_capacity(self.layers.len());
783+ let layers_hasher = RandomState::new();
773 // Use the edits to update all layers markers
784 // Use the edits to update all layers markers
774− if !edits.is_empty() {
775− fn point_add(a: Point, b: Point) -> Point {
785+ fn point_add(a: Point, b: Point) -> Point {
776− if b.row > 0 {
786+ if b.row > 0 {
777− Point::new(a.row.saturating_add(b.row), b.column)
787+ Point::new(a.row.saturating_add(b.row), b.column)
778− } else {
788+ } else {
779− Point::new(0, a.column.saturating_add(b.column))
789+ Point::new(0, a.column.saturating_add(b.column))
780− }
781 }
790 }
791+ }
782− fn point_sub(a: Point, b: Point) -> Point {
792+ fn point_sub(a: Point, b: Point) -> Point {
783− if a.row > b.row {
793+ if a.row > b.row {
784− Point::new(a.row.saturating_sub(b.row), a.column)
794+ Point::new(a.row.saturating_sub(b.row), a.column)
785− } else {
795+ } else {
786− Point::new(0, a.column.saturating_sub(b.column))
796+ Point::new(0, a.column.saturating_sub(b.column))
787− }
788 }
797 }
798+ }
789
799
790− for layer in self.layers.values_mut() {
800+ for (layer_id, layer) in self.layers.iter_mut() {
791− // The root layer always covers the whole range (0..usize::MAX)
801+ // The root layer always covers the whole range (0..usize::MAX)
792− if layer.depth == 0 {
802+ if layer.depth == 0 {
793− layer.flags = LayerUpdateFlags::MODIFIED;
803+ layer.flags = LayerUpdateFlags::MODIFIED;
794− continue;
804+ continue;
795− }
805+ }
796
806
807+ if !edits.is_empty() {
797 for range in &mut layer.ranges {
808 for range in &mut layer.ranges {
798 // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720
809 // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720
799 for edit in edits.iter().rev() {
810 for edit in edits.iter().rev() {
@@ -858,6 +869,12 @@ impl Syntax {
858 }
869 }
859 }
870 }
860 }
871 }
872+
873+ let hash = layers_hasher.hash_one(layer);
874+ // Safety: insert_no_grow is unsafe because it assumes that the table
875+ // has enough capacity to hold additional elements.
876+ // This is always the case as we reserved enough capacity above.
877+ unsafe { layers_table.insert_no_grow(hash, layer_id) };
861 }
878 }
862
879
863 PARSER.with(|ts_parser| {
880 PARSER.with(|ts_parser| {
@@ -982,27 +999,23 @@ impl Syntax {
982 let depth = layer.depth + 1;
999 let depth = layer.depth + 1;
983 // TODO: can't inline this since matches borrows self.layers
1000 // TODO: can't inline this since matches borrows self.layers
984 for (config, ranges) in injections {
1001 for (config, ranges) in injections {
1002+ let new_layer = LanguageLayer {
1003+ tree: None,
1004+ config,
1005+ depth,
1006+ ranges,
1007+ flags: LayerUpdateFlags::empty(),
1008+ };
1009+
985− // Find an existing layer
1010+ // Find an identical existing layer
986− let layer = self
1011+ let layer = layers_table
1012+ .get(layers_hasher.hash_one(&new_layer), |&it| {
987− .layers
1013+ self.layers[it] == new_layer
988− .iter_mut()
989− .find(|(_, layer)| {
990− layer.depth == depth && // TODO: track parent id instead
991− layer.config.language == config.language && layer.ranges == ranges
992 })
1014 })
993− .map(|(id, _layer)| id);
1015+ .copied();
994
1016
995 // ...or insert a new one.
1017 // ...or insert a new one.
996− let layer_id = layer.unwrap_or_else(|| {
1018+ let layer_id = layer.unwrap_or_else(|| self.layers.insert(new_layer));
997− self.layers.insert(LanguageLayer {
998− tree: None,
999− config,
1000− depth,
1001− ranges,
1002− // set the modified flag to ensure the layer is parsed
1003− flags: LayerUpdateFlags::empty(),
1004− })
1005− });
1006
1019
1007 queue.push_back(layer_id);
1020 queue.push_back(layer_id);
1008 }
1021 }
@@ -1139,6 +1152,34 @@ pub struct LanguageLayer {
1139 flags: LayerUpdateFlags,
1152 flags: LayerUpdateFlags,
1140}
1153}
1141
1154
1155+/// This PartialEq implementation only checks if that
1156+/// two layers are theoretically identical (meaning they highlight the same text range with the same language).
1157+/// It does not check whether the layers have the same internal treesitter
1158+/// state.
1159+impl PartialEq for LanguageLayer {
1160+ fn eq(&self, other: &Self) -> bool {
1161+ self.depth == other.depth
1162+ && self.config.language == other.config.language
1163+ && self.ranges == other.ranges
1164+ }
1165+}
1166+
1167+/// Hash implementation belongs to PartialEq implementation above.
1168+/// See its documentation for details.
1169+impl Hash for LanguageLayer {
1170+ fn hash<H: Hasher>(&self, state: &mut H) {
1171+ self.depth.hash(state);
1172+ // The transmute is necessary here because tree_sitter::Language does not derive Hash at the moment.
1173+ // However it does use #[repr] transparent so the transmute here is safe
1174+ // as `Language` (which `Grammar` is an alias for) is just a newtype wrapper around a (thin) pointer.
1175+ // This is also compatible with the PartialEq implementation of language
1176+ // as that is just a pointer comparison.
1177+ let language: *const () = unsafe { transmute(self.config.language) };
1178+ language.hash(state);
1179+ self.ranges.hash(state);
1180+ }
1181+}
1182+
1142impl LanguageLayer {
1183impl LanguageLayer {
1143 pub fn tree(&self) -> &Tree {
1184 pub fn tree(&self) -> &Tree {
1144 // TODO: no unwrap
1185 // TODO: no unwrap