Merge pull request #73 from zed-industries/grammars-cursor

Max Brunsfeld created

Initial syntax support

Change summary

Cargo.lock                                               |  29 
Cargo.toml                                               |  11 
gpui/Cargo.toml                                          |   2 
gpui/grammars/context-predicate/Cargo.toml               |  26 
gpui/grammars/context-predicate/binding.gyp              |   2 
gpui/grammars/context-predicate/bindings/node/binding.cc |   0 
gpui/grammars/context-predicate/bindings/node/index.js   |  19 
gpui/grammars/context-predicate/bindings/rust/build.rs   |  40 
gpui/grammars/context-predicate/bindings/rust/lib.rs     |  52 
gpui/grammars/context-predicate/index.js                 |  13 
gpui/grammars/context-predicate/package-lock.json        |  14 
gpui/grammars/context-predicate/package.json             |   4 
gpui/grammars/context-predicate/src/parser.c             |  56 
gpui/grammars/context-predicate/src/tree_sitter/parser.h | 159 -
gpui/src/font_cache.rs                                   |  26 
gpui/src/platform/mac/fonts.rs                           |  60 
zed/Cargo.toml                                           |   3 
zed/assets/themes/light.toml                             |  13 
zed/languages/rust/brackets.scm                          |   6 
zed/languages/rust/config.toml                           |   8 
zed/languages/rust/highlights.scm                        |  63 
zed/src/editor/buffer/mod.rs                             | 734 ++++++++-
zed/src/editor/buffer/point.rs                           |  23 
zed/src/editor/buffer/rope.rs                            |  18 
zed/src/editor/buffer/selection.rs                       |  14 
zed/src/editor/buffer_view.rs                            | 332 ++++
zed/src/editor/display_map/fold_map.rs                   | 165 +
zed/src/editor/display_map/mod.rs                        | 263 ++
zed/src/editor/mod.rs                                    |  10 
zed/src/editor/movement.rs                               |  16 
zed/src/file_finder.rs                                   |  50 
zed/src/language.rs                                      | 152 ++
zed/src/lib.rs                                           |   7 
zed/src/main.rs                                          |  19 
zed/src/settings.rs                                      | 275 +++
zed/src/test.rs                                          |  13 
zed/src/workspace.rs                                     |  68 
zed/src/worktree.rs                                      |   5 
38 files changed, 2,315 insertions(+), 455 deletions(-)

Detailed changes

Cargo.lock šŸ”—

@@ -347,7 +347,7 @@ dependencies = [
  "tar",
  "target_build_utils",
  "term",
- "toml",
+ "toml 0.4.10",
  "uuid",
  "walkdir",
 ]
@@ -2703,15 +2703,33 @@ dependencies = [
 ]
 
 [[package]]
-name = "tree-sitter"
-version = "0.17.1"
+name = "toml"
+version = "0.5.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d18dcb776d3affaba6db04d11d645946d34a69b3172e588af96ce9fecd20faac"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde 1.0.125",
+]
+
+[[package]]
+name = "tree-sitter"
+version = "0.19.5"
+source = "git+https://github.com/tree-sitter/tree-sitter?rev=d72771a19f4143530b1cfd23808e344f1276e176#d72771a19f4143530b1cfd23808e344f1276e176"
 dependencies = [
  "cc",
  "regex",
 ]
 
+[[package]]
+name = "tree-sitter-rust"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784f7ef9cdbd4c895dc2d4bb785e95b4a5364a602eec803681db83d1927ddf15"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "ttf-parser"
 version = "0.9.0"
@@ -2987,5 +3005,8 @@ dependencies = [
  "smallvec",
  "smol",
  "tempdir",
+ "toml 0.5.8",
+ "tree-sitter",
+ "tree-sitter-rust",
  "unindent",
 ]

Cargo.toml šŸ”—

@@ -2,13 +2,14 @@
 members = ["zed", "gpui", "gpui_macros", "fsevent", "scoped_pool"]
 
 [patch.crates-io]
-async-task = {git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e"}
+async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
+tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "d72771a19f4143530b1cfd23808e344f1276e176" }
 
 # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
-cocoa = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"}
-cocoa-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"}
-core-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"}
-core-graphics = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"}
+cocoa = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" }
+cocoa-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" }
+core-foundation = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" }
+core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737" }
 
 [profile.dev]
 split-debuginfo = "unpacked"

gpui/Cargo.toml šŸ”—

@@ -26,7 +26,7 @@ serde_json = "1.0.64"
 smallvec = {version = "1.6", features = ["union"]}
 smol = "1.2"
 tiny-skia = "0.5"
-tree-sitter = "0.17"
+tree-sitter = "0.19"
 usvg = "0.14"
 
 [build-dependencies]

gpui/grammars/context-predicate/Cargo.toml šŸ”—

@@ -0,0 +1,26 @@
+[package]
+name = "tree-sitter-context-predicate"
+description = "context-predicate grammar for the tree-sitter parsing library"
+version = "0.0.1"
+keywords = ["incremental", "parsing", "context-predicate"]
+categories = ["parsing", "text-editors"]
+repository = "https://github.com/tree-sitter/tree-sitter-javascript"
+edition = "2018"
+license = "MIT"
+
+build = "bindings/rust/build.rs"
+include = [
+  "bindings/rust/*",
+  "grammar.js",
+  "queries/*",
+  "src/*",
+]
+
+[lib]
+path = "bindings/rust/lib.rs"
+
+[dependencies]
+tree-sitter = "0.19.3"
+
+[build-dependencies]
+cc = "1.0"

gpui/grammars/context-predicate/bindings/node/index.js šŸ”—

@@ -0,0 +1,19 @@
+try {
+  module.exports = require("../../build/Release/tree_sitter_context_predicate_binding");
+} catch (error1) {
+  if (error1.code !== 'MODULE_NOT_FOUND') {
+    throw error1;
+  }
+  try {
+    module.exports = require("../../build/Debug/tree_sitter_context_predicate_binding");
+  } catch (error2) {
+    if (error2.code !== 'MODULE_NOT_FOUND') {
+      throw error2;
+    }
+    throw error1
+  }
+}
+
+try {
+  module.exports.nodeTypeInfo = require("../../src/node-types.json");
+} catch (_) {}

gpui/grammars/context-predicate/bindings/rust/build.rs šŸ”—

@@ -0,0 +1,40 @@
+fn main() {
+    let src_dir = std::path::Path::new("src");
+
+    let mut c_config = cc::Build::new();
+    c_config.include(&src_dir);
+    c_config
+        .flag_if_supported("-Wno-unused-parameter")
+        .flag_if_supported("-Wno-unused-but-set-variable")
+        .flag_if_supported("-Wno-trigraphs");
+    let parser_path = src_dir.join("parser.c");
+    c_config.file(&parser_path);
+
+    // If your language uses an external scanner written in C,
+    // then include this block of code:
+
+    /*
+    let scanner_path = src_dir.join("scanner.c");
+    c_config.file(&scanner_path);
+    println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
+    */
+
+    c_config.compile("parser");
+    println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
+
+    // If your language uses an external scanner written in C++,
+    // then include this block of code:
+
+    /*
+    let mut cpp_config = cc::Build::new();
+    cpp_config.cpp(true);
+    cpp_config.include(&src_dir);
+    cpp_config
+        .flag_if_supported("-Wno-unused-parameter")
+        .flag_if_supported("-Wno-unused-but-set-variable");
+    let scanner_path = src_dir.join("scanner.cc");
+    cpp_config.file(&scanner_path);
+    cpp_config.compile("scanner");
+    println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
+    */
+}

gpui/grammars/context-predicate/bindings/rust/lib.rs šŸ”—

@@ -0,0 +1,52 @@
+//! This crate provides context_predicate language support for the [tree-sitter][] parsing library.
+//!
+//! Typically, you will use the [language][language func] function to add this language to a
+//! tree-sitter [Parser][], and then use the parser to parse some code:
+//!
+//! ```
+//! let code = "";
+//! let mut parser = tree_sitter::Parser::new();
+//! parser.set_language(tree_sitter_context_predicate::language()).expect("Error loading context_predicate grammar");
+//! let tree = parser.parse(code, None).unwrap();
+//! ```
+//!
+//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
+//! [language func]: fn.language.html
+//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html
+//! [tree-sitter]: https://tree-sitter.github.io/
+
+use tree_sitter::Language;
+
+extern "C" {
+    fn tree_sitter_context_predicate() -> Language;
+}
+
+/// Get the tree-sitter [Language][] for this grammar.
+///
+/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
+pub fn language() -> Language {
+    unsafe { tree_sitter_context_predicate() }
+}
+
+/// The content of the [`node-types.json`][] file for this grammar.
+///
+/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types
+pub const NODE_TYPES: &'static str = include_str!("../../src/node-types.json");
+
+// Uncomment these to include any queries that this grammar contains
+
+// pub const HIGHLIGHTS_QUERY: &'static str = include_str!("../../queries/highlights.scm");
+// pub const INJECTIONS_QUERY: &'static str = include_str!("../../queries/injections.scm");
+// pub const LOCALS_QUERY: &'static str = include_str!("../../queries/locals.scm");
+// pub const TAGS_QUERY: &'static str = include_str!("../../queries/tags.scm");
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test_can_load_grammar() {
+        let mut parser = tree_sitter::Parser::new();
+        parser
+            .set_language(super::language())
+            .expect("Error loading context_predicate language");
+    }
+}

gpui/grammars/context-predicate/index.js šŸ”—

@@ -1,13 +0,0 @@
-try {
-  module.exports = require("./build/Release/tree_sitter_context_predicate_binding");
-} catch (error) {
-  try {
-    module.exports = require("./build/Debug/tree_sitter_context_predicate_binding");
-  } catch (_) {
-    throw error
-  }
-}
-
-try {
-  module.exports.nodeTypeInfo = require("./src/node-types.json");
-} catch (_) { }

gpui/grammars/context-predicate/package-lock.json šŸ”—

@@ -9,7 +9,7 @@
         "nan": "^2.14.0"
       },
       "devDependencies": {
-        "tree-sitter-cli": "^0.18.3"
+        "tree-sitter-cli": "^0.19.5"
       }
     },
     "node_modules/nan": {
@@ -18,9 +18,9 @@
       "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
     },
     "node_modules/tree-sitter-cli": {
-      "version": "0.18.3",
-      "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.18.3.tgz",
-      "integrity": "sha512-ntN8Siljy7dlazb4cSYtZCfibaNppIy6RIr/XGt44GW1hAy/yuTLtKVi4kqYlImB4/7H0AjktcSlQRmI8zLNng==",
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.19.5.tgz",
+      "integrity": "sha512-kRzKrUAwpDN9AjA3b0tPBwT1hd8N2oQvvvHup2OEsX6mdsSMLmAvR+NSqK9fe05JrRbVvG8mbteNUQsxlMQohQ==",
       "dev": true,
       "hasInstallScript": true,
       "bin": {
@@ -35,9 +35,9 @@
       "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ=="
     },
     "tree-sitter-cli": {
-      "version": "0.18.3",
-      "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.18.3.tgz",
-      "integrity": "sha512-ntN8Siljy7dlazb4cSYtZCfibaNppIy6RIr/XGt44GW1hAy/yuTLtKVi4kqYlImB4/7H0AjktcSlQRmI8zLNng==",
+      "version": "0.19.5",
+      "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.19.5.tgz",
+      "integrity": "sha512-kRzKrUAwpDN9AjA3b0tPBwT1hd8N2oQvvvHup2OEsX6mdsSMLmAvR+NSqK9fe05JrRbVvG8mbteNUQsxlMQohQ==",
       "dev": true
     }
   }

gpui/grammars/context-predicate/package.json šŸ”—

@@ -1,8 +1,8 @@
 {
   "name": "tree-sitter-context-predicate",
-  "main": "index.js",
+  "main": "bindings/node",
   "devDependencies": {
-    "tree-sitter-cli": "^0.18.3"
+    "tree-sitter-cli": "^0.19.5"
   },
   "dependencies": {
     "nan": "^2.14.0"

gpui/grammars/context-predicate/src/parser.c šŸ”—

@@ -5,7 +5,7 @@
 #pragma GCC diagnostic ignored "-Wmissing-field-initializers"
 #endif
 
-#define LANGUAGE_VERSION 12
+#define LANGUAGE_VERSION 13
 #define STATE_COUNT 18
 #define LARGE_STATE_COUNT 6
 #define SYMBOL_COUNT 17
@@ -14,6 +14,7 @@
 #define EXTERNAL_TOKEN_COUNT 0
 #define FIELD_COUNT 3
 #define MAX_ALIAS_SEQUENCE_LENGTH 3
+#define PRODUCTION_ID_COUNT 3
 
 enum {
   sym_identifier = 1,
@@ -34,7 +35,7 @@ enum {
   sym_parenthesized = 16,
 };
 
-static const char *ts_symbol_names[] = {
+static const char * const ts_symbol_names[] = {
   [ts_builtin_sym_end] = "end",
   [sym_identifier] = "identifier",
   [anon_sym_BANG] = "!",
@@ -54,7 +55,7 @@ static const char *ts_symbol_names[] = {
   [sym_parenthesized] = "parenthesized",
 };
 
-static TSSymbol ts_symbol_map[] = {
+static const TSSymbol ts_symbol_map[] = {
   [ts_builtin_sym_end] = ts_builtin_sym_end,
   [sym_identifier] = sym_identifier,
   [anon_sym_BANG] = anon_sym_BANG,
@@ -151,14 +152,14 @@ enum {
   field_right = 3,
 };
 
-static const char *ts_field_names[] = {
+static const char * const ts_field_names[] = {
   [0] = NULL,
   [field_expression] = "expression",
   [field_left] = "left",
   [field_right] = "right",
 };
 
-static const TSFieldMapSlice ts_field_map_slices[3] = {
+static const TSFieldMapSlice ts_field_map_slices[PRODUCTION_ID_COUNT] = {
   [1] = {.index = 0, .length = 1},
   [2] = {.index = 1, .length = 2},
 };
@@ -171,11 +172,11 @@ static const TSFieldMapEntry ts_field_map_entries[] = {
     {field_right, 2},
 };
 
-static TSSymbol ts_alias_sequences[3][MAX_ALIAS_SEQUENCE_LENGTH] = {
+static const TSSymbol ts_alias_sequences[PRODUCTION_ID_COUNT][MAX_ALIAS_SEQUENCE_LENGTH] = {
   [0] = {0},
 };
 
-static uint16_t ts_non_terminal_alias_map[] = {
+static const uint16_t ts_non_terminal_alias_map[] = {
   0,
 };
 
@@ -279,7 +280,7 @@ static bool ts_lex(TSLexer *lexer, TSStateId state) {
   }
 }
 
-static TSLexMode ts_lex_modes[STATE_COUNT] = {
+static const TSLexMode ts_lex_modes[STATE_COUNT] = {
   [0] = {.lex_state = 0},
   [1] = {.lex_state = 1},
   [2] = {.lex_state = 1},
@@ -300,7 +301,7 @@ static TSLexMode ts_lex_modes[STATE_COUNT] = {
   [17] = {.lex_state = 0},
 };
 
-static uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = {
+static const uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = {
   [0] = {
     [ts_builtin_sym_end] = ACTIONS(1),
     [sym_identifier] = ACTIONS(1),
@@ -375,7 +376,7 @@ static uint16_t ts_parse_table[LARGE_STATE_COUNT][SYMBOL_COUNT] = {
   },
 };
 
-static uint16_t ts_small_parse_table[] = {
+static const uint16_t ts_small_parse_table[] = {
   [0] = 3,
     ACTIONS(11), 1,
       anon_sym_EQ_EQ,
@@ -448,7 +449,7 @@ static uint16_t ts_small_parse_table[] = {
       sym_identifier,
 };
 
-static uint32_t ts_small_parse_table_map[] = {
+static const uint32_t ts_small_parse_table_map[] = {
   [SMALL_STATE(6)] = 0,
   [SMALL_STATE(7)] = 13,
   [SMALL_STATE(8)] = 20,
@@ -463,7 +464,7 @@ static uint32_t ts_small_parse_table_map[] = {
   [SMALL_STATE(17)] = 85,
 };
 
-static TSParseActionEntry ts_parse_actions[] = {
+static const TSParseActionEntry ts_parse_actions[] = {
   [0] = {.entry = {.count = 0, .reusable = false}},
   [1] = {.entry = {.count = 1, .reusable = false}}, RECOVER(),
   [3] = {.entry = {.count = 1, .reusable = true}}, SHIFT(6),
@@ -495,30 +496,31 @@ extern "C" {
 #endif
 
 extern const TSLanguage *tree_sitter_context_predicate(void) {
-  static TSLanguage language = {
+  static const TSLanguage language = {
     .version = LANGUAGE_VERSION,
     .symbol_count = SYMBOL_COUNT,
     .alias_count = ALIAS_COUNT,
     .token_count = TOKEN_COUNT,
     .external_token_count = EXTERNAL_TOKEN_COUNT,
-    .symbol_names = ts_symbol_names,
-    .symbol_metadata = ts_symbol_metadata,
-    .parse_table = (const uint16_t *)ts_parse_table,
-    .parse_actions = ts_parse_actions,
-    .lex_modes = ts_lex_modes,
-    .alias_sequences = (const TSSymbol *)ts_alias_sequences,
-    .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH,
-    .lex_fn = ts_lex,
+    .state_count = STATE_COUNT,
+    .large_state_count = LARGE_STATE_COUNT,
+    .production_id_count = PRODUCTION_ID_COUNT,
     .field_count = FIELD_COUNT,
-    .field_map_slices = (const TSFieldMapSlice *)ts_field_map_slices,
-    .field_map_entries = (const TSFieldMapEntry *)ts_field_map_entries,
+    .max_alias_sequence_length = MAX_ALIAS_SEQUENCE_LENGTH,
+    .parse_table = &ts_parse_table[0][0],
+    .small_parse_table = ts_small_parse_table,
+    .small_parse_table_map = ts_small_parse_table_map,
+    .parse_actions = ts_parse_actions,
+    .symbol_names = ts_symbol_names,
     .field_names = ts_field_names,
-    .large_state_count = LARGE_STATE_COUNT,
-    .small_parse_table = (const uint16_t *)ts_small_parse_table,
-    .small_parse_table_map = (const uint32_t *)ts_small_parse_table_map,
+    .field_map_slices = ts_field_map_slices,
+    .field_map_entries = ts_field_map_entries,
+    .symbol_metadata = ts_symbol_metadata,
     .public_symbol_map = ts_symbol_map,
     .alias_map = ts_non_terminal_alias_map,
-    .state_count = STATE_COUNT,
+    .alias_sequences = &ts_alias_sequences[0][0],
+    .lex_modes = ts_lex_modes,
+    .lex_fn = ts_lex,
   };
   return &language;
 }

gpui/grammars/context-predicate/src/tree_sitter/parser.h šŸ”—

@@ -13,6 +13,8 @@ extern "C" {
 #define ts_builtin_sym_end 0
 #define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
 
+typedef uint16_t TSStateId;
+
 #ifndef TREE_SITTER_API_H_
 typedef uint16_t TSSymbol;
 typedef uint16_t TSFieldId;
@@ -30,12 +32,10 @@ typedef struct {
   uint16_t length;
 } TSFieldMapSlice;
 
-typedef uint16_t TSStateId;
-
 typedef struct {
-  bool visible : 1;
-  bool named : 1;
-  bool supertype: 1;
+  bool visible;
+  bool named;
+  bool supertype;
 } TSSymbolMetadata;
 
 typedef struct TSLexer TSLexer;
@@ -57,21 +57,21 @@ typedef enum {
   TSParseActionTypeRecover,
 } TSParseActionType;
 
-typedef struct {
-  union {
-    struct {
-      TSStateId state;
-      bool extra : 1;
-      bool repetition : 1;
-    } shift;
-    struct {
-      TSSymbol symbol;
-      int16_t dynamic_precedence;
-      uint8_t child_count;
-      uint8_t production_id;
-    } reduce;
-  } params;
-  TSParseActionType type : 4;
+typedef union {
+  struct {
+    uint8_t type;
+    TSStateId state;
+    bool extra;
+    bool repetition;
+  } shift;
+  struct {
+    uint8_t type;
+    uint8_t child_count;
+    TSSymbol symbol;
+    int16_t dynamic_precedence;
+    uint16_t production_id;
+  } reduce;
+  uint8_t type;
 } TSParseAction;
 
 typedef struct {
@@ -83,7 +83,7 @@ typedef union {
   TSParseAction action;
   struct {
     uint8_t count;
-    bool reusable : 1;
+    bool reusable;
   } entry;
 } TSParseActionEntry;
 
@@ -93,13 +93,24 @@ struct TSLanguage {
   uint32_t alias_count;
   uint32_t token_count;
   uint32_t external_token_count;
-  const char **symbol_names;
-  const TSSymbolMetadata *symbol_metadata;
+  uint32_t state_count;
+  uint32_t large_state_count;
+  uint32_t production_id_count;
+  uint32_t field_count;
+  uint16_t max_alias_sequence_length;
   const uint16_t *parse_table;
+  const uint16_t *small_parse_table;
+  const uint32_t *small_parse_table_map;
   const TSParseActionEntry *parse_actions;
-  const TSLexMode *lex_modes;
+  const char * const *symbol_names;
+  const char * const *field_names;
+  const TSFieldMapSlice *field_map_slices;
+  const TSFieldMapEntry *field_map_entries;
+  const TSSymbolMetadata *symbol_metadata;
+  const TSSymbol *public_symbol_map;
+  const uint16_t *alias_map;
   const TSSymbol *alias_sequences;
-  uint16_t max_alias_sequence_length;
+  const TSLexMode *lex_modes;
   bool (*lex_fn)(TSLexer *, TSStateId);
   bool (*keyword_lex_fn)(TSLexer *, TSStateId);
   TSSymbol keyword_capture_token;
@@ -112,16 +123,6 @@ struct TSLanguage {
     unsigned (*serialize)(void *, char *);
     void (*deserialize)(void *, const char *, unsigned);
   } external_scanner;
-  uint32_t field_count;
-  const TSFieldMapSlice *field_map_slices;
-  const TSFieldMapEntry *field_map_entries;
-  const char **field_names;
-  uint32_t large_state_count;
-  const uint16_t *small_parse_table;
-  const uint32_t *small_parse_table_map;
-  const TSSymbol *public_symbol_map;
-  const uint16_t *alias_map;
-  uint32_t state_count;
 };
 
 /*
@@ -170,66 +171,50 @@ struct TSLanguage {
 
 #define ACTIONS(id) id
 
-#define SHIFT(state_value)                \
-  {                                       \
-    {                                     \
-      .params = {                         \
-        .shift = {                        \
-          .state = state_value            \
-        }                                 \
-      },                                  \
-      .type = TSParseActionTypeShift      \
-    }                                     \
-  }
+#define SHIFT(state_value)            \
+  {{                                  \
+    .shift = {                        \
+      .type = TSParseActionTypeShift, \
+      .state = state_value            \
+    }                                 \
+  }}
 
 #define SHIFT_REPEAT(state_value)     \
-  {                                   \
-    {                                 \
-      .params = {                     \
-        .shift = {                    \
-          .state = state_value,       \
-          .repetition = true          \
-        }                             \
-      },                              \
-      .type = TSParseActionTypeShift  \
+  {{                                  \
+    .shift = {                        \
+      .type = TSParseActionTypeShift, \
+      .state = state_value,           \
+      .repetition = true              \
     }                                 \
-  }
-
-#define RECOVER()                        \
-  {                                      \
-    { .type = TSParseActionTypeRecover } \
-  }
+  }}
 
 #define SHIFT_EXTRA()                 \
-  {                                   \
-    {                                 \
-      .params = {                     \
-        .shift = {                    \
-          .extra = true               \
-        }                             \
-      },                              \
-      .type = TSParseActionTypeShift  \
+  {{                                  \
+    .shift = {                        \
+      .type = TSParseActionTypeShift, \
+      .extra = true                   \
     }                                 \
-  }
+  }}
 
 #define REDUCE(symbol_val, child_count_val, ...) \
-  {                                              \
-    {                                            \
-      .params = {                                \
-        .reduce = {                              \
-          .symbol = symbol_val,                  \
-          .child_count = child_count_val,        \
-          __VA_ARGS__                            \
-        },                                       \
-      },                                         \
-      .type = TSParseActionTypeReduce            \
-    }                                            \
-  }
-
-#define ACCEPT_INPUT()                  \
-  {                                     \
-    { .type = TSParseActionTypeAccept } \
-  }
+  {{                                             \
+    .reduce = {                                  \
+      .type = TSParseActionTypeReduce,           \
+      .symbol = symbol_val,                      \
+      .child_count = child_count_val,            \
+      __VA_ARGS__                                \
+    },                                           \
+  }}
+
+#define RECOVER()                    \
+  {{                                 \
+    .type = TSParseActionTypeRecover \
+  }}
+
+#define ACCEPT_INPUT()              \
+  {{                                \
+    .type = TSParseActionTypeAccept \
+  }}
 
 #ifdef __cplusplus
 }

gpui/src/font_cache.rs šŸ”—

@@ -161,3 +161,29 @@ impl FontCache {
         metric * font_size / self.metric(font_id, |m| m.units_per_em as f32)
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        fonts::{Style, Weight},
+        platform::{test, Platform as _},
+    };
+
+    #[test]
+    fn test_select_font() {
+        let platform = test::platform();
+        let fonts = FontCache::new(platform.fonts());
+        let arial = fonts.load_family(&["Arial"]).unwrap();
+        let arial_regular = fonts.select_font(arial, &Properties::new()).unwrap();
+        let arial_italic = fonts
+            .select_font(arial, &Properties::new().style(Style::Italic))
+            .unwrap();
+        let arial_bold = fonts
+            .select_font(arial, &Properties::new().weight(Weight::BOLD))
+            .unwrap();
+        assert_ne!(arial_regular, arial_italic);
+        assert_ne!(arial_regular, arial_bold);
+        assert_ne!(arial_italic, arial_bold);
+    }
+}

gpui/src/platform/mac/fonts.rs šŸ”—

@@ -322,6 +322,9 @@ mod tests {
         let menlo_regular = fonts.select_font(&menlo, &Properties::new())?;
         let menlo_italic = fonts.select_font(&menlo, &Properties::new().style(Style::Italic))?;
         let menlo_bold = fonts.select_font(&menlo, &Properties::new().weight(Weight::BOLD))?;
+        assert_ne!(menlo_regular, menlo_italic);
+        assert_ne!(menlo_regular, menlo_bold);
+        assert_ne!(menlo_italic, menlo_bold);
 
         let line = fonts.layout_str(
             "hello world",
@@ -371,32 +374,33 @@ mod tests {
         Ok(())
     }
 
-    // #[test]
-    // fn test_rasterize_glyph() {
-    //     use std::{fs::File, io::BufWriter, path::Path};
-
-    //     let fonts = FontSystem::new();
-    //     let font_ids = fonts.load_family("Fira Code").unwrap();
-    //     let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
-    //     let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
-
-    //     const VARIANTS: usize = 1;
-    //     for i in 0..VARIANTS {
-    //         let variant = i as f32 / VARIANTS as f32;
-    //         let (bounds, bytes) = fonts
-    //             .rasterize_glyph(font_id, 16.0, glyph_id, vec2f(variant, variant), 2.)
-    //             .unwrap();
-
-    //         let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
-    //         let path = Path::new(&name);
-    //         let file = File::create(path).unwrap();
-    //         let ref mut w = BufWriter::new(file);
-
-    //         let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32);
-    //         encoder.set_color(png::ColorType::Grayscale);
-    //         encoder.set_depth(png::BitDepth::Eight);
-    //         let mut writer = encoder.write_header().unwrap();
-    //         writer.write_image_data(&bytes).unwrap();
-    //     }
-    // }
+    #[test]
+    #[ignore]
+    fn test_rasterize_glyph() {
+        use std::{fs::File, io::BufWriter, path::Path};
+
+        let fonts = FontSystem::new();
+        let font_ids = fonts.load_family("Fira Code").unwrap();
+        let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
+        let glyph_id = fonts.glyph_for_char(font_id, 'G').unwrap();
+
+        const VARIANTS: usize = 1;
+        for i in 0..VARIANTS {
+            let variant = i as f32 / VARIANTS as f32;
+            let (bounds, bytes) = fonts
+                .rasterize_glyph(font_id, 16.0, glyph_id, vec2f(variant, variant), 2.)
+                .unwrap();
+
+            let name = format!("/Users/as-cii/Desktop/twog-{}.png", i);
+            let path = Path::new(&name);
+            let file = File::create(path).unwrap();
+            let ref mut w = BufWriter::new(file);
+
+            let mut encoder = png::Encoder::new(w, bounds.width() as u32, bounds.height() as u32);
+            encoder.set_color(png::ColorType::Grayscale);
+            encoder.set_depth(png::BitDepth::Eight);
+            let mut writer = encoder.write_header().unwrap();
+            writer.write_image_data(&bytes).unwrap();
+        }
+    }
 }

zed/Cargo.toml šŸ”—

@@ -38,6 +38,9 @@ similar = "1.3"
 simplelog = "0.9"
 smallvec = {version = "1.6", features = ["union"]}
 smol = "1.2.5"
+toml = "0.5"
+tree-sitter = "0.19.5"
+tree-sitter-rust = "0.19.0"
 
 [dev-dependencies]
 cargo-bundle = "0.5.0"

zed/assets/themes/light.toml šŸ”—

@@ -0,0 +1,13 @@
+[ui]
+background = 0xffffff
+line_numbers = 0x237791
+text = 0x0d0d0d
+
+[syntax]
+keyword = 0xaf00db
+function = 0x795e26
+string = 0xa31515
+type = 0x267599
+number = 0x0d885b
+comment = 0x048204
+property = 0x001080

zed/languages/rust/brackets.scm šŸ”—

@@ -0,0 +1,6 @@
+("(" @open ")" @close)
+("[" @open "]" @close)
+("{" @open "}" @close)
+("<" @open ">" @close)
+("\"" @open "\"" @close)
+(closure_parameters "|" @open "|" @close)

zed/languages/rust/config.toml šŸ”—

@@ -0,0 +1,8 @@
+name = "Rust"
+path_suffixes = ["rs"]
+bracket_pairs = [
+    { start = "{", end = "}" },
+    { start = "[", end = "]" },
+    { start = "(", end = ")" },
+    { start = "<", end = ">" },
+]

zed/languages/rust/highlights.scm šŸ”—

@@ -0,0 +1,63 @@
+(type_identifier) @type
+(primitive_type) @type.builtin
+
+(field_identifier) @property
+
+(call_expression
+  function: [
+    (identifier) @function
+    (scoped_identifier
+      name: (identifier) @function)
+    (field_expression
+      field: (field_identifier) @function.method)
+  ])
+
+(function_item name: (identifier) @function.definition)
+(function_signature_item name: (identifier) @function.definition)
+
+[
+  "async"
+  "break"
+  "const"
+  "continue"
+  "default"
+  "dyn"
+  "else"
+  "enum"
+  "extern"
+  "for"
+  "fn"
+  "if"
+  "in"
+  "impl"
+  "let"
+  "loop"
+  "macro_rules!"
+  "match"
+  "mod"
+  "move"
+  "pub"
+  "return"
+  "static"
+  "struct"
+  "trait"
+  "type"
+  "use"
+  "where"
+  "while"
+  "union"
+  "unsafe"
+  (mutable_specifier)
+  (super)
+] @keyword
+
+[
+  (string_literal)
+  (raw_string_literal)
+  (char_literal)
+] @string
+
+[
+  (line_comment)
+  (block_comment)
+] @comment

zed/src/editor/buffer/mod.rs šŸ”—

@@ -4,15 +4,19 @@ pub mod rope;
 mod selection;
 
 pub use anchor::*;
+use parking_lot::Mutex;
 pub use point::*;
-pub use rope::{ChunksIter, Rope, TextSummary};
+pub use rope::{Chunks, Rope, TextSummary};
 use seahash::SeaHasher;
 pub use selection::*;
 use similar::{ChangeTag, TextDiff};
+use tree_sitter::{InputEdit, Parser, QueryCursor};
 
 use crate::{
     editor::Bias,
+    language::{Language, Tree},
     operation_queue::{self, OperationQueue},
+    settings::{StyleId, ThemeMap},
     sum_tree::{self, FilterCursor, SeekBias, SumTree},
     time::{self, ReplicaId},
     worktree::FileHandle,
@@ -21,11 +25,12 @@ use anyhow::{anyhow, Result};
 use gpui::{AppContext, Entity, ModelContext, Task};
 use lazy_static::lazy_static;
 use std::{
+    cell::RefCell,
     cmp,
     hash::BuildHasher,
     iter::{self, Iterator},
     mem,
-    ops::Range,
+    ops::{Deref, DerefMut, Range},
     str,
     sync::Arc,
     time::{Duration, Instant, SystemTime, UNIX_EPOCH},
@@ -56,6 +61,50 @@ type HashMap<K, V> = std::collections::HashMap<K, V>;
 #[cfg(not(test))]
 type HashSet<T> = std::collections::HashSet<T>;
 
+thread_local! {
+    static PARSER: RefCell<Parser> = RefCell::new(Parser::new());
+}
+
+lazy_static! {
+    static ref QUERY_CURSORS: Mutex<Vec<QueryCursor>> = Default::default();
+}
+
+struct QueryCursorHandle(Option<QueryCursor>);
+
+impl QueryCursorHandle {
+    fn new() -> Self {
+        QueryCursorHandle(Some(
+            QUERY_CURSORS
+                .lock()
+                .pop()
+                .unwrap_or_else(|| QueryCursor::new()),
+        ))
+    }
+}
+
+impl Deref for QueryCursorHandle {
+    type Target = QueryCursor;
+
+    fn deref(&self) -> &Self::Target {
+        self.0.as_ref().unwrap()
+    }
+}
+
+impl DerefMut for QueryCursorHandle {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        self.0.as_mut().unwrap()
+    }
+}
+
+impl Drop for QueryCursorHandle {
+    fn drop(&mut self) {
+        let mut cursor = self.0.take().unwrap();
+        cursor.set_byte_range(0..usize::MAX);
+        cursor.set_point_range(Point::zero().into()..Point::MAX.into());
+        QUERY_CURSORS.lock().push(cursor)
+    }
+}
+
 pub struct Buffer {
     fragments: SumTree<Fragment>,
     visible_text: Rope,
@@ -68,6 +117,9 @@ pub struct Buffer {
     undo_map: UndoMap,
     history: History,
     file: Option<FileHandle>,
+    language: Option<Arc<Language>>,
+    syntax_tree: Mutex<Option<SyntaxTree>>,
+    is_parsing: bool,
     selections: HashMap<SelectionSetId, Arc<[Selection]>>,
     pub selections_last_update: SelectionsVersion,
     deferred_ops: OperationQueue<Operation>,
@@ -77,6 +129,13 @@ pub struct Buffer {
     lamport_clock: time::Lamport,
 }
 
+#[derive(Clone)]
+struct SyntaxTree {
+    tree: Tree,
+    parsed: bool,
+    version: time::Global,
+}
+
 #[derive(Clone)]
 struct Transaction {
     start: time::Global,
@@ -238,16 +297,18 @@ impl UndoMap {
 }
 
 struct Edits<'a, F: Fn(&FragmentSummary) -> bool> {
-    cursor: FilterCursor<'a, F, Fragment, usize>,
+    deleted_text: &'a Rope,
+    cursor: FilterCursor<'a, F, Fragment, FragmentTextSummary>,
     undos: &'a UndoMap,
     since: time::Global,
     delta: isize,
 }
 
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
 pub struct Edit {
     pub old_range: Range<usize>,
     pub new_range: Range<usize>,
+    pub old_lines: Point,
 }
 
 impl Edit {
@@ -357,22 +418,24 @@ impl Buffer {
         base_text: T,
         ctx: &mut ModelContext<Self>,
     ) -> Self {
-        Self::build(replica_id, History::new(base_text.into()), None, ctx)
+        Self::build(replica_id, History::new(base_text.into()), None, None, ctx)
     }
 
     pub fn from_history(
         replica_id: ReplicaId,
         history: History,
         file: Option<FileHandle>,
+        language: Option<Arc<Language>>,
         ctx: &mut ModelContext<Self>,
     ) -> Self {
-        Self::build(replica_id, history, file, ctx)
+        Self::build(replica_id, history, file, language, ctx)
     }
 
     fn build(
         replica_id: ReplicaId,
         history: History,
         file: Option<FileHandle>,
+        language: Option<Arc<Language>>,
         ctx: &mut ModelContext<Self>,
     ) -> Self {
         let saved_mtime;
@@ -461,7 +524,7 @@ impl Buffer {
             );
         }
 
-        Self {
+        let mut result = Self {
             visible_text,
             deleted_text: Rope::new(),
             fragments,
@@ -472,6 +535,9 @@ impl Buffer {
             undo_map: Default::default(),
             history,
             file,
+            syntax_tree: Mutex::new(None),
+            is_parsing: false,
+            language,
             saved_mtime,
             selections: HashMap::default(),
             selections_last_update: 0,
@@ -480,11 +546,18 @@ impl Buffer {
             replica_id,
             local_clock: time::Local::new(replica_id),
             lamport_clock: time::Lamport::new(replica_id),
-        }
+        };
+        result.reparse(ctx);
+        result
     }
 
-    pub fn snapshot(&self) -> Rope {
-        self.visible_text.clone()
+    pub fn snapshot(&self) -> Snapshot {
+        Snapshot {
+            text: self.visible_text.clone(),
+            tree: self.syntax_tree(),
+            language: self.language.clone(),
+            query_cursor: QueryCursorHandle::new(),
+        }
     }
 
     pub fn file(&self) -> Option<&FileHandle> {
@@ -496,13 +569,13 @@ impl Buffer {
         new_file: Option<FileHandle>,
         ctx: &mut ModelContext<Self>,
     ) -> Task<Result<()>> {
-        let snapshot = self.snapshot();
+        let text = self.visible_text.clone();
         let version = self.version.clone();
         let file = self.file.clone();
 
         ctx.spawn(|handle, mut ctx| async move {
             if let Some(file) = new_file.as_ref().or(file.as_ref()) {
-                let result = ctx.read(|ctx| file.save(snapshot, ctx.as_ref())).await;
+                let result = ctx.read(|ctx| file.save(text, ctx.as_ref())).await;
                 if result.is_ok() {
                     handle.update(&mut ctx, |me, ctx| me.did_save(version, new_file, ctx));
                 }
@@ -529,6 +602,154 @@ impl Buffer {
         ctx.emit(Event::Saved);
     }
 
+    pub fn syntax_tree(&self) -> Option<Tree> {
+        if let Some(syntax_tree) = self.syntax_tree.lock().as_mut() {
+            let mut edited = false;
+            let mut delta = 0_isize;
+            for Edit {
+                old_range,
+                new_range,
+                old_lines,
+            } in self.edits_since(syntax_tree.version.clone())
+            {
+                let start_offset = (old_range.start as isize + delta) as usize;
+                let start_point = self.visible_text.to_point(start_offset);
+                let old_bytes = old_range.end - old_range.start;
+                let new_bytes = new_range.end - new_range.start;
+                syntax_tree.tree.edit(&InputEdit {
+                    start_byte: start_offset,
+                    old_end_byte: start_offset + old_bytes,
+                    new_end_byte: start_offset + new_bytes,
+                    start_position: start_point.into(),
+                    old_end_position: (start_point + old_lines).into(),
+                    new_end_position: self.visible_text.to_point(start_offset + new_bytes).into(),
+                });
+                delta += new_bytes as isize - old_bytes as isize;
+                edited = true;
+            }
+            syntax_tree.parsed &= !edited;
+            syntax_tree.version = self.version();
+            Some(syntax_tree.tree.clone())
+        } else {
+            None
+        }
+    }
+
+    pub fn is_parsing(&self) -> bool {
+        self.is_parsing
+    }
+
+    fn should_reparse(&self) -> bool {
+        if let Some(syntax_tree) = self.syntax_tree.lock().as_ref() {
+            !syntax_tree.parsed || syntax_tree.version != self.version
+        } else {
+            self.language.is_some()
+        }
+    }
+
+    fn reparse(&mut self, ctx: &mut ModelContext<Self>) {
+        // Avoid spawning a new parsing task if the buffer is already being reparsed
+        // due to an earlier edit.
+        if self.is_parsing {
+            return;
+        }
+
+        if let Some(language) = self.language.clone() {
+            self.is_parsing = true;
+            ctx.spawn(|handle, mut ctx| async move {
+                while handle.read_with(&ctx, |this, _| this.should_reparse()) {
+                    // The parse tree is out of date, so grab the syntax tree to synchronously
+                    // splice all the edits that have happened since the last parse.
+                    let new_tree = handle.update(&mut ctx, |this, _| this.syntax_tree());
+                    let (new_text, new_version) = handle
+                        .read_with(&ctx, |this, _| (this.visible_text.clone(), this.version()));
+
+                    // Parse the current text in a background thread.
+                    let new_tree = ctx
+                        .background_executor()
+                        .spawn({
+                            let language = language.clone();
+                            async move { Self::parse_text(&new_text, new_tree, &language) }
+                        })
+                        .await;
+
+                    handle.update(&mut ctx, |this, ctx| {
+                        *this.syntax_tree.lock() = Some(SyntaxTree {
+                            tree: new_tree,
+                            parsed: true,
+                            version: new_version,
+                        });
+                        ctx.emit(Event::Reparsed);
+                        ctx.notify();
+                    });
+                }
+                handle.update(&mut ctx, |this, _| this.is_parsing = false);
+            })
+            .detach();
+        }
+    }
+
+    fn parse_text(text: &Rope, old_tree: Option<Tree>, language: &Language) -> Tree {
+        PARSER.with(|parser| {
+            let mut parser = parser.borrow_mut();
+            parser
+                .set_language(language.grammar)
+                .expect("incompatible grammar");
+            let mut chunks = text.chunks_in_range(0..text.len());
+            let tree = parser
+                .parse_with(
+                    &mut move |offset, _| {
+                        chunks.seek(offset);
+                        chunks.next().unwrap_or("").as_bytes()
+                    },
+                    old_tree.as_ref(),
+                )
+                .unwrap();
+            tree
+        })
+    }
+
+    pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
+        if let Some(tree) = self.syntax_tree() {
+            let root = tree.root_node();
+            let range = range.start.to_offset(self)..range.end.to_offset(self);
+            let mut node = root.descendant_for_byte_range(range.start, range.end);
+            while node.map_or(false, |n| n.byte_range() == range) {
+                node = node.unwrap().parent();
+            }
+            node.map(|n| n.byte_range())
+        } else {
+            None
+        }
+    }
+
+    pub fn enclosing_bracket_ranges<T: ToOffset>(
+        &self,
+        range: Range<T>,
+    ) -> Option<(Range<usize>, Range<usize>)> {
+        let (lang, tree) = self.language.as_ref().zip(self.syntax_tree())?;
+        let open_capture_ix = lang.brackets_query.capture_index_for_name("open")?;
+        let close_capture_ix = lang.brackets_query.capture_index_for_name("close")?;
+
+        // Find bracket pairs that *inclusively* contain the given range.
+        let range = range.start.to_offset(self).saturating_sub(1)..range.end.to_offset(self) + 1;
+        let mut cursor = QueryCursorHandle::new();
+        let matches = cursor.set_byte_range(range).matches(
+            &lang.brackets_query,
+            tree.root_node(),
+            TextProvider(&self.visible_text),
+        );
+
+        // Get the ranges of the innermost pair of brackets.
+        matches
+            .filter_map(|mat| {
+                let open = mat.nodes_for_capture_index(open_capture_ix).next()?;
+                let close = mat.nodes_for_capture_index(close_capture_ix).next()?;
+                Some((open.byte_range(), close.byte_range()))
+            })
+            .min_by_key(|(open_range, close_range)| close_range.end - open_range.start)
+    }
+
     fn diff(&self, new_text: Arc<str>, ctx: &AppContext) -> Task<Diff> {
         // TODO: it would be nice to not allocate here.
         let old_text = self.text();
@@ -620,17 +841,15 @@ impl Buffer {
         self.visible_text.max_point()
     }
 
-    pub fn line(&self, row: u32) -> String {
-        self.chars_at(Point::new(row, 0))
-            .take_while(|c| *c != '\n')
-            .collect()
+    pub fn row_count(&self) -> u32 {
+        self.max_point().row + 1
     }
 
     pub fn text(&self) -> String {
         self.text_for_range(0..self.len()).collect()
     }
 
-    pub fn text_for_range<'a, T: ToOffset>(&'a self, range: Range<T>) -> ChunksIter<'a> {
+    pub fn text_for_range<'a, T: ToOffset>(&'a self, range: Range<T>) -> Chunks<'a> {
         let start = range.start.to_offset(self);
         let end = range.end.to_offset(self);
         self.visible_text.chunks_in_range(start..end)
@@ -656,6 +875,7 @@ impl Buffer {
             .filter(move |summary| summary.max_version.changed_since(&since_2));
 
         Edits {
+            deleted_text: &self.deleted_text,
             cursor,
             undos: &self.undo_map,
             since,
@@ -720,6 +940,7 @@ impl Buffer {
 
                 if self.edits_since(since).next().is_some() {
                     self.did_edit(was_dirty, ctx);
+                    self.reparse(ctx);
                 }
             }
         }
@@ -741,17 +962,17 @@ impl Buffer {
         self.start_transaction_at(None, Instant::now())?;
 
         let new_text = new_text.into();
+        let old_ranges = old_ranges
+            .into_iter()
+            .map(|range| range.start.to_offset(self)..range.end.to_offset(self))
+            .collect::<Vec<Range<usize>>>();
+
         let new_text = if new_text.len() > 0 {
             Some(new_text)
         } else {
             None
         };
 
-        let old_ranges = old_ranges
-            .into_iter()
-            .map(|range| range.start.to_offset(self)..range.end.to_offset(self))
-            .collect::<Vec<Range<usize>>>();
-
         let has_new_text = new_text.is_some();
         let ops = self.splice_fragments(
             old_ranges
@@ -889,6 +1110,7 @@ impl Buffer {
             ctx.notify();
             if self.edits_since(old_version).next().is_some() {
                 self.did_edit(was_dirty, ctx);
+                self.reparse(ctx);
             }
         }
 
@@ -1114,6 +1336,7 @@ impl Buffer {
             ctx.notify();
             if self.edits_since(old_version).next().is_some() {
                 self.did_edit(was_dirty, ctx);
+                self.reparse(ctx);
             }
         }
 
@@ -1140,6 +1363,7 @@ impl Buffer {
             ctx.notify();
             if self.edits_since(old_version).next().is_some() {
                 self.did_edit(was_dirty, ctx);
+                self.reparse(ctx);
             }
         }
 
@@ -1884,6 +2108,9 @@ impl Clone for Buffer {
             selections_last_update: self.selections_last_update.clone(),
             deferred_ops: self.deferred_ops.clone(),
             file: self.file.clone(),
+            language: self.language.clone(),
+            syntax_tree: Mutex::new(self.syntax_tree.lock().clone()),
+            is_parsing: false,
             deferred_replicas: self.deferred_replicas.clone(),
             replica_id: self.replica_id,
             local_clock: self.local_clock.clone(),
@@ -1892,6 +2119,71 @@ impl Clone for Buffer {
     }
 }
 
+pub struct Snapshot {
+    text: Rope,
+    tree: Option<Tree>,
+    language: Option<Arc<Language>>,
+    query_cursor: QueryCursorHandle,
+}
+
+impl Snapshot {
+    pub fn len(&self) -> usize {
+        self.text.len()
+    }
+
+    pub fn text(&self) -> Rope {
+        self.text.clone()
+    }
+
+    pub fn text_for_range(&self, range: Range<usize>) -> Chunks {
+        self.text.chunks_in_range(range)
+    }
+
+    pub fn highlighted_text_for_range(&mut self, range: Range<usize>) -> HighlightedChunks {
+        let chunks = self.text.chunks_in_range(range.clone());
+        if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) {
+            let captures = self.query_cursor.set_byte_range(range.clone()).captures(
+                &language.highlight_query,
+                tree.root_node(),
+                TextProvider(&self.text),
+            );
+
+            HighlightedChunks {
+                range,
+                chunks,
+                highlights: Some(Highlights {
+                    captures,
+                    next_capture: None,
+                    stack: Default::default(),
+                    theme_mapping: language.theme_mapping(),
+                }),
+            }
+        } else {
+            HighlightedChunks {
+                range,
+                chunks,
+                highlights: None,
+            }
+        }
+    }
+
+    pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize {
+        self.text.clip_offset(offset, bias)
+    }
+
+    pub fn clip_point(&self, point: Point, bias: Bias) -> Point {
+        self.text.clip_point(point, bias)
+    }
+
+    pub fn to_offset(&self, point: Point) -> usize {
+        self.text.to_offset(point)
+    }
+
+    pub fn to_point(&self, offset: usize) -> Point {
+        self.text.to_point(offset)
+    }
+}
+
 struct RopeBuilder<'a> {
     old_visible_cursor: rope::Cursor<'a>,
     old_deleted_cursor: rope::Cursor<'a>,
@@ -1951,6 +2243,7 @@ pub enum Event {
     Saved,
     FileHandleChanged,
     Reloaded,
+    Reparsed,
 }
 
 impl Entity for Buffer {
@@ -1964,7 +2257,7 @@ impl<'a, F: Fn(&FragmentSummary) -> bool> Iterator for Edits<'a, F> {
         let mut change: Option<Edit> = None;
 
         while let Some(fragment) = self.cursor.item() {
-            let new_offset = *self.cursor.start();
+            let new_offset = self.cursor.start().visible;
             let old_offset = (new_offset as isize - self.delta) as usize;
 
             if !fragment.was_visible(&self.since, &self.undos) && fragment.visible {
@@ -1979,13 +2272,18 @@ impl<'a, F: Fn(&FragmentSummary) -> bool> Iterator for Edits<'a, F> {
                     change = Some(Edit {
                         old_range: old_offset..old_offset,
                         new_range: new_offset..new_offset + fragment.len(),
+                        old_lines: Point::zero(),
                     });
                     self.delta += fragment.len() as isize;
                 }
             } else if fragment.was_visible(&self.since, &self.undos) && !fragment.visible {
+                let deleted_start = self.cursor.start().deleted;
+                let old_lines = self.deleted_text.to_point(deleted_start + fragment.len())
+                    - self.deleted_text.to_point(deleted_start);
                 if let Some(ref mut change) = change {
                     if change.new_range.end == new_offset {
                         change.old_range.end += fragment.len();
+                        change.old_lines += &old_lines;
                         self.delta -= fragment.len() as isize;
                     } else {
                         break;
@@ -1994,6 +2292,7 @@ impl<'a, F: Fn(&FragmentSummary) -> bool> Iterator for Edits<'a, F> {
                     change = Some(Edit {
                         old_range: old_offset..old_offset + fragment.len(),
                         new_range: new_offset..new_offset,
+                        old_lines,
                     });
                     self.delta -= fragment.len() as isize;
                 }
@@ -2006,70 +2305,125 @@ impl<'a, F: Fn(&FragmentSummary) -> bool> Iterator for Edits<'a, F> {
     }
 }
 
-// pub fn diff(a: &[u16], b: &[u16]) -> Vec<Edit> {
-//     struct EditCollector<'a> {
-//         a: &'a [u16],
-//         b: &'a [u16],
-//         position: Point,
-//         changes: Vec<Edit>,
-//     }
-//
-//     impl<'a> diffs::Diff for EditCollector<'a> {
-//         type Error = ();
-//
-//         fn equal(&mut self, old: usize, _: usize, len: usize) -> Result<(), ()> {
-//             self.position += &Text::extent(&self.a[old..old + len]);
-//             Ok(())
-//         }
-//
-//         fn delete(&mut self, old: usize, len: usize) -> Result<(), ()> {
-//             self.changes.push(Edit {
-//                 range: self.position..self.position + &Text::extent(&self.a[old..old + len]),
-//                 chars: Vec::new(),
-//                 new_char_count: Point::zero(),
-//             });
-//             Ok(())
-//         }
-//
-//         fn insert(&mut self, _: usize, new: usize, new_len: usize) -> Result<(), ()> {
-//             let new_char_count = Text::extent(&self.b[new..new + new_len]);
-//             self.changes.push(Edit {
-//                 range: self.position..self.position,
-//                 chars: Vec::from(&self.b[new..new + new_len]),
-//                 new_char_count,
-//             });
-//             self.position += &new_char_count;
-//             Ok(())
-//         }
-//
-//         fn replace(
-//             &mut self,
-//             old: usize,
-//             old_len: usize,
-//             new: usize,
-//             new_len: usize,
-//         ) -> Result<(), ()> {
-//             let old_extent = text::extent(&self.a[old..old + old_len]);
-//             let new_char_count = text::extent(&self.b[new..new + new_len]);
-//             self.changes.push(Edit {
-//                 range: self.position..self.position + &old_extent,
-//                 chars: Vec::from(&self.b[new..new + new_len]),
-//                 new_char_count,
-//             });
-//             self.position += &new_char_count;
-//             Ok(())
-//         }
-//     }
-//
-//     let mut collector = diffs::Replace::new(EditCollector {
-//         a,
-//         b,
-//         position: Point::zero(),
-//         changes: Vec::new(),
-//     });
-//     diffs::myers::diff(&mut collector, a, 0, a.len(), b, 0, b.len()).unwrap();
-//     collector.into_inner().changes
-// }
+struct ByteChunks<'a>(rope::Chunks<'a>);
+
+impl<'a> Iterator for ByteChunks<'a> {
+    type Item = &'a [u8];
+
+    fn next(&mut self) -> Option<Self::Item> {
+        self.0.next().map(str::as_bytes)
+    }
+}
+
+struct TextProvider<'a>(&'a Rope);
+
+impl<'a> tree_sitter::TextProvider<'a> for TextProvider<'a> {
+    type I = ByteChunks<'a>;
+
+    fn text(&mut self, node: tree_sitter::Node) -> Self::I {
+        ByteChunks(self.0.chunks_in_range(node.byte_range()))
+    }
+}
+
+struct Highlights<'a> {
+    captures: tree_sitter::QueryCaptures<'a, 'a, TextProvider<'a>>,
+    next_capture: Option<(tree_sitter::QueryMatch<'a, 'a>, usize)>,
+    stack: Vec<(usize, StyleId)>,
+    theme_mapping: ThemeMap,
+}
+
+pub struct HighlightedChunks<'a> {
+    range: Range<usize>,
+    chunks: Chunks<'a>,
+    highlights: Option<Highlights<'a>>,
+}
+
+impl<'a> HighlightedChunks<'a> {
+    pub fn seek(&mut self, offset: usize) {
+        self.range.start = offset;
+        self.chunks.seek(self.range.start);
+        if let Some(highlights) = self.highlights.as_mut() {
+            highlights
+                .stack
+                .retain(|(end_offset, _)| *end_offset > offset);
+            if let Some((mat, capture_ix)) = &highlights.next_capture {
+                let capture = mat.captures[*capture_ix as usize];
+                if offset >= capture.node.start_byte() {
+                    let next_capture_end = capture.node.end_byte();
+                    if offset < next_capture_end {
+                        highlights.stack.push((
+                            next_capture_end,
+                            highlights.theme_mapping.get(capture.index),
+                        ));
+                    }
+                    highlights.next_capture.take();
+                }
+            }
+            highlights.captures.set_byte_range(self.range.clone());
+        }
+    }
+
+    pub fn offset(&self) -> usize {
+        self.range.start
+    }
+}
+
+impl<'a> Iterator for HighlightedChunks<'a> {
+    type Item = (&'a str, StyleId);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let mut next_capture_start = usize::MAX;
+
+        if let Some(highlights) = self.highlights.as_mut() {
+            while let Some((parent_capture_end, _)) = highlights.stack.last() {
+                if *parent_capture_end <= self.range.start {
+                    highlights.stack.pop();
+                } else {
+                    break;
+                }
+            }
+
+            if highlights.next_capture.is_none() {
+                highlights.next_capture = highlights.captures.next();
+            }
+
+            while let Some((mat, capture_ix)) = highlights.next_capture.as_ref() {
+                let capture = mat.captures[*capture_ix as usize];
+                if self.range.start < capture.node.start_byte() {
+                    next_capture_start = capture.node.start_byte();
+                    break;
+                } else {
+                    let style_id = highlights.theme_mapping.get(capture.index);
+                    highlights.stack.push((capture.node.end_byte(), style_id));
+                    highlights.next_capture = highlights.captures.next();
+                }
+            }
+        }
+
+        if let Some(chunk) = self.chunks.peek() {
+            let chunk_start = self.range.start;
+            let mut chunk_end = (self.chunks.offset() + chunk.len()).min(next_capture_start);
+            let mut style_id = StyleId::default();
+            if let Some((parent_capture_end, parent_style_id)) =
+                self.highlights.as_ref().and_then(|h| h.stack.last())
+            {
+                chunk_end = chunk_end.min(*parent_capture_end);
+                style_id = *parent_style_id;
+            }
+
+            let slice =
+                &chunk[chunk_start - self.chunks.offset()..chunk_end - self.chunks.offset()];
+            self.range.start = chunk_end;
+            if self.range.start == self.chunks.offset() + chunk.len() {
+                self.chunks.next().unwrap();
+            }
+
+            Some((slice, style_id))
+        } else {
+            None
+        }
+    }
+}
 
 #[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
 struct FragmentId(Arc<[u16]>);
@@ -2342,11 +2696,11 @@ impl ToPoint for usize {
 mod tests {
     use super::*;
     use crate::{
-        test::temp_tree,
+        test::{build_app_state, temp_tree},
         util::RandomCharIter,
         worktree::{Worktree, WorktreeHandle},
     };
-    use gpui::App;
+    use gpui::{App, ModelHandle};
     use rand::prelude::*;
     use serde_json::json;
     use std::{
@@ -2475,6 +2829,7 @@ mod tests {
                     for Edit {
                         old_range,
                         new_range,
+                        ..
                     } in buffer.edits_since(old_buffer.version.clone())
                     {
                         let old_len = old_range.end - old_range.start;
@@ -2812,7 +3167,7 @@ mod tests {
 
             let file1 = app.update(|ctx| tree.file("file1", ctx)).await;
             let buffer1 = app.add_model(|ctx| {
-                Buffer::from_history(0, History::new("abc".into()), Some(file1), ctx)
+                Buffer::from_history(0, History::new("abc".into()), Some(file1), None, ctx)
             });
             let events = Rc::new(RefCell::new(Vec::new()));
 
@@ -2877,7 +3232,7 @@ mod tests {
                     move |_, event, _| events.borrow_mut().push(event.clone())
                 });
 
-                Buffer::from_history(0, History::new("abc".into()), Some(file2), ctx)
+                Buffer::from_history(0, History::new("abc".into()), Some(file2), None, ctx)
             });
 
             fs::remove_file(dir.path().join("file2")).unwrap();
@@ -2896,7 +3251,7 @@ mod tests {
                     move |_, event, _| events.borrow_mut().push(event.clone())
                 });
 
-                Buffer::from_history(0, History::new("abc".into()), Some(file3), ctx)
+                Buffer::from_history(0, History::new("abc".into()), Some(file3), None, ctx)
             });
 
             tree.flush_fs_events(&app).await;
@@ -2923,7 +3278,13 @@ mod tests {
         let abs_path = dir.path().join("the-file");
         let file = app.update(|ctx| tree.file("the-file", ctx)).await;
         let buffer = app.add_model(|ctx| {
-            Buffer::from_history(0, History::new(initial_contents.into()), Some(file), ctx)
+            Buffer::from_history(
+                0,
+                History::new(initial_contents.into()),
+                Some(file),
+                None,
+                ctx,
+            )
         });
 
         // Add a cursor at the start of each row.
@@ -3191,6 +3552,193 @@ mod tests {
         }
     }
 
+    #[gpui::test]
+    async fn test_reparse(mut ctx: gpui::TestAppContext) {
+        let app_state = ctx.read(build_app_state);
+        let rust_lang = app_state.language_registry.select_language("test.rs");
+        assert!(rust_lang.is_some());
+
+        let buffer = ctx.add_model(|ctx| {
+            let text = "fn a() {}".into();
+            let buffer = Buffer::from_history(0, History::new(text), None, rust_lang.cloned(), ctx);
+            assert!(buffer.is_parsing());
+            assert!(buffer.syntax_tree().is_none());
+            buffer
+        });
+
+        // Wait for the initial text to parse
+        buffer
+            .condition(&ctx, |buffer, _| !buffer.is_parsing())
+            .await;
+        assert_eq!(
+            get_tree_sexp(&buffer, &ctx),
+            concat!(
+                "(source_file (function_item name: (identifier) ",
+                "parameters: (parameters) ",
+                "body: (block)))"
+            )
+        );
+
+        // Perform some edits (add parameter and variable reference)
+        // Parsing doesn't begin until the transaction is complete
+        buffer.update(&mut ctx, |buf, ctx| {
+            buf.start_transaction(None).unwrap();
+
+            let offset = buf.text().find(")").unwrap();
+            buf.edit(vec![offset..offset], "b: C", Some(ctx)).unwrap();
+            assert!(!buf.is_parsing());
+
+            let offset = buf.text().find("}").unwrap();
+            buf.edit(vec![offset..offset], " d; ", Some(ctx)).unwrap();
+            assert!(!buf.is_parsing());
+
+            buf.end_transaction(None, Some(ctx)).unwrap();
+            assert_eq!(buf.text(), "fn a(b: C) { d; }");
+            assert!(buf.is_parsing());
+        });
+        buffer
+            .condition(&ctx, |buffer, _| !buffer.is_parsing())
+            .await;
+        assert_eq!(
+            get_tree_sexp(&buffer, &ctx),
+            concat!(
+                "(source_file (function_item name: (identifier) ",
+                    "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
+                    "body: (block (identifier))))"
+            )
+        );
+
+        // Perform a series of edits without waiting for the current parse to complete:
+        // * turn identifier into a field expression
+        // * turn field expression into a method call
+        // * add a turbofish to the method call
+        buffer.update(&mut ctx, |buf, ctx| {
+            let offset = buf.text().find(";").unwrap();
+            buf.edit(vec![offset..offset], ".e", Some(ctx)).unwrap();
+            assert_eq!(buf.text(), "fn a(b: C) { d.e; }");
+            assert!(buf.is_parsing());
+        });
+        buffer.update(&mut ctx, |buf, ctx| {
+            let offset = buf.text().find(";").unwrap();
+            buf.edit(vec![offset..offset], "(f)", Some(ctx)).unwrap();
+            assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }");
+            assert!(buf.is_parsing());
+        });
+        buffer.update(&mut ctx, |buf, ctx| {
+            let offset = buf.text().find("(f)").unwrap();
+            buf.edit(vec![offset..offset], "::<G>", Some(ctx)).unwrap();
+            assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
+            assert!(buf.is_parsing());
+        });
+        buffer
+            .condition(&ctx, |buffer, _| !buffer.is_parsing())
+            .await;
+        assert_eq!(
+            get_tree_sexp(&buffer, &ctx),
+            concat!(
+                "(source_file (function_item name: (identifier) ",
+                    "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
+                    "body: (block (call_expression ",
+                        "function: (generic_function ",
+                            "function: (field_expression value: (identifier) field: (field_identifier)) ",
+                            "type_arguments: (type_arguments (type_identifier))) ",
+                            "arguments: (arguments (identifier))))))",
+            )
+        );
+
+        buffer.update(&mut ctx, |buf, ctx| {
+            buf.undo(Some(ctx));
+            assert_eq!(buf.text(), "fn a() {}");
+            assert!(buf.is_parsing());
+        });
+        buffer
+            .condition(&ctx, |buffer, _| !buffer.is_parsing())
+            .await;
+        assert_eq!(
+            get_tree_sexp(&buffer, &ctx),
+            concat!(
+                "(source_file (function_item name: (identifier) ",
+                "parameters: (parameters) ",
+                "body: (block)))"
+            )
+        );
+
+        buffer.update(&mut ctx, |buf, ctx| {
+            buf.redo(Some(ctx));
+            assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
+            assert!(buf.is_parsing());
+        });
+        buffer
+            .condition(&ctx, |buffer, _| !buffer.is_parsing())
+            .await;
+        assert_eq!(
+            get_tree_sexp(&buffer, &ctx),
+            concat!(
+                "(source_file (function_item name: (identifier) ",
+                    "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
+                    "body: (block (call_expression ",
+                        "function: (generic_function ",
+                            "function: (field_expression value: (identifier) field: (field_identifier)) ",
+                            "type_arguments: (type_arguments (type_identifier))) ",
+                            "arguments: (arguments (identifier))))))",
+            )
+        );
+
+        fn get_tree_sexp(buffer: &ModelHandle<Buffer>, ctx: &gpui::TestAppContext) -> String {
+            buffer.read_with(ctx, |buffer, _| {
+                buffer.syntax_tree().unwrap().root_node().to_sexp()
+            })
+        }
+    }
+
+    #[gpui::test]
+    async fn test_enclosing_bracket_ranges(mut ctx: gpui::TestAppContext) {
+        use unindent::Unindent as _;
+
+        let app_state = ctx.read(build_app_state);
+        let rust_lang = app_state.language_registry.select_language("test.rs");
+        assert!(rust_lang.is_some());
+
+        let buffer = ctx.add_model(|ctx| {
+            let text = "
+                mod x {
+                    mod y {
+                        
+                    }
+                }
+            "
+            .unindent()
+            .into();
+            Buffer::from_history(0, History::new(text), None, rust_lang.cloned(), ctx)
+        });
+        buffer
+            .condition(&ctx, |buffer, _| !buffer.is_parsing())
+            .await;
+        buffer.read_with(&ctx, |buf, _| {
+            assert_eq!(
+                buf.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
+                Some((
+                    Point::new(0, 6)..Point::new(0, 7),
+                    Point::new(4, 0)..Point::new(4, 1)
+                ))
+            );
+            assert_eq!(
+                buf.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
+                Some((
+                    Point::new(1, 10)..Point::new(1, 11),
+                    Point::new(3, 4)..Point::new(3, 5)
+                ))
+            );
+            assert_eq!(
+                buf.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
+                Some((
+                    Point::new(1, 10)..Point::new(1, 11),
+                    Point::new(3, 4)..Point::new(3, 5)
+                ))
+            );
+        });
+    }
+
     impl Buffer {
         fn random_byte_range(&mut self, start_offset: usize, rng: &mut impl Rng) -> Range<usize> {
             let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right);

zed/src/editor/buffer/point.rs šŸ”—

@@ -10,6 +10,11 @@ pub struct Point {
 }
 
 impl Point {
+    pub const MAX: Self = Self {
+        row: u32::MAX,
+        column: u32::MAX,
+    };
+
     pub fn new(row: u32, column: u32) -> Self {
         Point { row, column }
     }
@@ -98,3 +103,21 @@ impl Ord for Point {
         }
     }
 }
+
+impl Into<tree_sitter::Point> for Point {
+    fn into(self) -> tree_sitter::Point {
+        tree_sitter::Point {
+            row: self.row as usize,
+            column: self.column as usize,
+        }
+    }
+}
+
+impl From<tree_sitter::Point> for Point {
+    fn from(point: tree_sitter::Point) -> Self {
+        Self {
+            row: point.row as u32,
+            column: point.column as u32,
+        }
+    }
+}

zed/src/editor/buffer/rope.rs šŸ”—

@@ -118,12 +118,12 @@ impl Rope {
         self.chunks_in_range(start..self.len()).flat_map(str::chars)
     }
 
-    pub fn chunks<'a>(&'a self) -> ChunksIter<'a> {
+    pub fn chunks<'a>(&'a self) -> Chunks<'a> {
         self.chunks_in_range(0..self.len())
     }
 
-    pub fn chunks_in_range<'a>(&'a self, range: Range<usize>) -> ChunksIter<'a> {
-        ChunksIter::new(self, range)
+    pub fn chunks_in_range<'a>(&'a self, range: Range<usize>) -> Chunks<'a> {
+        Chunks::new(self, range)
     }
 
     pub fn to_point(&self, offset: usize) -> Point {
@@ -268,12 +268,12 @@ impl<'a> Cursor<'a> {
     }
 }
 
-pub struct ChunksIter<'a> {
+pub struct Chunks<'a> {
     chunks: sum_tree::Cursor<'a, Chunk, usize, usize>,
     range: Range<usize>,
 }
 
-impl<'a> ChunksIter<'a> {
+impl<'a> Chunks<'a> {
     pub fn new(rope: &'a Rope, range: Range<usize>) -> Self {
         let mut chunks = rope.chunks.cursor();
         chunks.seek(&range.start, SeekBias::Right, &());
@@ -284,11 +284,13 @@ impl<'a> ChunksIter<'a> {
         self.range.start.max(*self.chunks.start())
     }
 
-    pub fn advance_to(&mut self, offset: usize) {
+    pub fn seek(&mut self, offset: usize) {
         if offset >= self.chunks.end() {
             self.chunks.seek_forward(&offset, SeekBias::Right, &());
-            self.range.start = offset;
+        } else {
+            self.chunks.seek(&offset, SeekBias::Right, &());
         }
+        self.range.start = offset;
     }
 
     pub fn peek(&self) -> Option<&'a str> {
@@ -304,7 +306,7 @@ impl<'a> ChunksIter<'a> {
     }
 }
 
-impl<'a> Iterator for ChunksIter<'a> {
+impl<'a> Iterator for Chunks<'a> {
     type Item = &'a str;
 
     fn next(&mut self) -> Option<Self::Item> {

zed/src/editor/buffer/selection.rs šŸ”—

@@ -1,6 +1,6 @@
 use crate::{
     editor::{
-        buffer::{Anchor, Buffer, Point, ToPoint},
+        buffer::{Anchor, Buffer, Point, ToOffset as _, ToPoint as _},
         display_map::DisplayMap,
         Bias, DisplayPoint,
     },
@@ -61,7 +61,7 @@ impl Selection {
         }
     }
 
-    pub fn range(&self, buffer: &Buffer) -> Range<Point> {
+    pub fn point_range(&self, buffer: &Buffer) -> Range<Point> {
         let start = self.start.to_point(buffer);
         let end = self.end.to_point(buffer);
         if self.reversed {
@@ -71,6 +71,16 @@ impl Selection {
         }
     }
 
+    pub fn offset_range(&self, buffer: &Buffer) -> Range<usize> {
+        let start = self.start.to_offset(buffer);
+        let end = self.end.to_offset(buffer);
+        if self.reversed {
+            end..start
+        } else {
+            start..end
+        }
+    }
+
     pub fn display_range(&self, map: &DisplayMap, app: &AppContext) -> Range<DisplayPoint> {
         let start = self.start.to_display_point(map, app);
         let end = self.end.to_display_point(map, app);

zed/src/editor/buffer_view.rs šŸ”—

@@ -2,7 +2,12 @@ use super::{
     buffer, movement, Anchor, Bias, Buffer, BufferElement, DisplayMap, DisplayPoint, Point,
     Selection, SelectionGoal, SelectionSetId, ToOffset, ToPoint,
 };
-use crate::{settings::Settings, util::post_inc, workspace, worktree::FileHandle};
+use crate::{
+    settings::{Settings, StyleId},
+    util::post_inc,
+    workspace,
+    worktree::FileHandle,
+};
 use anyhow::Result;
 use gpui::{
     color::ColorU, fonts::Properties as FontProperties, geometry::vector::Vector2F,
@@ -161,6 +166,21 @@ pub fn init(app: &mut MutableAppContext) {
             "buffer:add_selection_below",
             Some("BufferView"),
         ),
+        Binding::new(
+            "alt-up",
+            "buffer:select_larger_syntax_node",
+            Some("BufferView"),
+        ),
+        Binding::new(
+            "alt-down",
+            "buffer:select_smaller_syntax_node",
+            Some("BufferView"),
+        ),
+        Binding::new(
+            "ctrl-m",
+            "buffer:move_to_enclosing_bracket",
+            Some("BufferView"),
+        ),
         Binding::new("pageup", "buffer:page_up", Some("BufferView")),
         Binding::new("pagedown", "buffer:page_down", Some("BufferView")),
         Binding::new("alt-cmd-[", "buffer:fold", Some("BufferView")),
@@ -265,6 +285,18 @@ pub fn init(app: &mut MutableAppContext) {
         "buffer:add_selection_below",
         BufferView::add_selection_below,
     );
+    app.add_action(
+        "buffer:select_larger_syntax_node",
+        BufferView::select_larger_syntax_node,
+    );
+    app.add_action(
+        "buffer:select_smaller_syntax_node",
+        BufferView::select_smaller_syntax_node,
+    );
+    app.add_action(
+        "buffer:move_to_enclosing_bracket",
+        BufferView::move_to_enclosing_bracket,
+    );
     app.add_action("buffer:page_up", BufferView::page_up);
     app.add_action("buffer:page_down", BufferView::page_down);
     app.add_action("buffer:fold", BufferView::fold);
@@ -295,6 +327,7 @@ pub struct BufferView {
     pending_selection: Option<Selection>,
     next_selection_id: usize,
     add_selections_state: Option<AddSelectionsState>,
+    select_larger_syntax_node_stack: Vec<Vec<Selection>>,
     scroll_position: Mutex<Vector2F>,
     autoscroll_requested: Mutex<bool>,
     settings: watch::Receiver<Settings>,
@@ -354,6 +387,7 @@ impl BufferView {
             pending_selection: None,
             next_selection_id,
             add_selections_state: None,
+            select_larger_syntax_node_stack: Vec::new(),
             scroll_position: Mutex::new(Vector2F::zero()),
             autoscroll_requested: Mutex::new(false),
             settings,
@@ -690,7 +724,7 @@ impl BufferView {
         {
             let buffer = self.buffer.read(ctx);
             for selection in &mut selections {
-                let range = selection.range(buffer);
+                let range = selection.point_range(buffer);
                 if range.start == range.end {
                     let head = selection
                         .head()
@@ -717,7 +751,7 @@ impl BufferView {
         {
             let buffer = self.buffer.read(ctx);
             for selection in &mut selections {
-                let range = selection.range(buffer);
+                let range = selection.point_range(buffer);
                 if range.start == range.end {
                     let head = selection
                         .head()
@@ -896,7 +930,7 @@ impl BufferView {
         let mut contiguous_selections = Vec::new();
         while let Some(selection) = selections.next() {
             // Accumulate contiguous regions of rows that we want to move.
-            contiguous_selections.push(selection.range(buffer));
+            contiguous_selections.push(selection.point_range(buffer));
             let (mut buffer_rows, mut display_rows) =
                 selection.buffer_rows_for_display_rows(false, &self.display_map, app);
             while let Some(next_selection) = selections.peek() {
@@ -905,7 +939,7 @@ impl BufferView {
                 if next_buffer_rows.start <= buffer_rows.end {
                     buffer_rows.end = next_buffer_rows.end;
                     display_rows.end = next_display_rows.end;
-                    contiguous_selections.push(next_selection.range(buffer));
+                    contiguous_selections.push(next_selection.point_range(buffer));
                     selections.next().unwrap();
                 } else {
                     break;
@@ -980,7 +1014,7 @@ impl BufferView {
         let mut contiguous_selections = Vec::new();
         while let Some(selection) = selections.next() {
             // Accumulate contiguous regions of rows that we want to move.
-            contiguous_selections.push(selection.range(buffer));
+            contiguous_selections.push(selection.point_range(buffer));
             let (mut buffer_rows, mut display_rows) =
                 selection.buffer_rows_for_display_rows(false, &self.display_map, app);
             while let Some(next_selection) = selections.peek() {
@@ -989,7 +1023,7 @@ impl BufferView {
                 if next_buffer_rows.start <= buffer_rows.end {
                     buffer_rows.end = next_buffer_rows.end;
                     display_rows.end = next_display_rows.end;
-                    contiguous_selections.push(next_selection.range(buffer));
+                    contiguous_selections.push(next_selection.point_range(buffer));
                     selections.next().unwrap();
                 } else {
                     break;
@@ -1613,7 +1647,7 @@ impl BufferView {
         let mut to_unfold = Vec::new();
         let mut new_selections = Vec::new();
         for selection in self.selections(app) {
-            let range = selection.range(buffer).sorted();
+            let range = selection.point_range(buffer).sorted();
             if range.start.row != range.end.row {
                 new_selections.push(Selection {
                     id: post_inc(&mut self.next_selection_id),
@@ -1654,7 +1688,7 @@ impl BufferView {
         self.add_selection(false, ctx);
     }
 
-    pub fn add_selection(&mut self, above: bool, ctx: &mut ViewContext<Self>) {
+    fn add_selection(&mut self, above: bool, ctx: &mut ViewContext<Self>) {
         use super::RangeExt;
 
         let app = ctx.as_ref();
@@ -1746,6 +1780,77 @@ impl BufferView {
         }
     }
 
+    pub fn select_larger_syntax_node(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        let app = ctx.as_ref();
+        let buffer = self.buffer.read(app);
+
+        let mut stack = mem::take(&mut self.select_larger_syntax_node_stack);
+        let mut selected_larger_node = false;
+        let old_selections = self.selections(app).to_vec();
+        let mut new_selections = Vec::new();
+        for selection in &old_selections {
+            let old_range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer);
+            let mut new_range = old_range.clone();
+            while let Some(containing_range) = buffer.range_for_syntax_ancestor(new_range.clone()) {
+                new_range = containing_range;
+                if !self.display_map.intersects_fold(new_range.start, app)
+                    && !self.display_map.intersects_fold(new_range.end, app)
+                {
+                    break;
+                }
+            }
+
+            selected_larger_node |= new_range != old_range;
+            new_selections.push(Selection {
+                id: selection.id,
+                start: buffer.anchor_before(new_range.start),
+                end: buffer.anchor_before(new_range.end),
+                reversed: selection.reversed,
+                goal: SelectionGoal::None,
+            });
+        }
+
+        if selected_larger_node {
+            stack.push(old_selections);
+            self.update_selections(new_selections, true, ctx);
+        }
+        self.select_larger_syntax_node_stack = stack;
+    }
+
+    pub fn select_smaller_syntax_node(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        let mut stack = mem::take(&mut self.select_larger_syntax_node_stack);
+        if let Some(selections) = stack.pop() {
+            self.update_selections(selections, true, ctx);
+        }
+        self.select_larger_syntax_node_stack = stack;
+    }
+
+    pub fn move_to_enclosing_bracket(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
+        use super::RangeExt as _;
+
+        let buffer = self.buffer.read(ctx.as_ref());
+        let mut selections = self.selections(ctx.as_ref()).to_vec();
+        for selection in &mut selections {
+            let selection_range = selection.offset_range(buffer);
+            if let Some((open_range, close_range)) =
+                buffer.enclosing_bracket_ranges(selection_range.clone())
+            {
+                let close_range = close_range.to_inclusive();
+                let destination = if close_range.contains(&selection_range.start)
+                    && close_range.contains(&selection_range.end)
+                {
+                    open_range.end
+                } else {
+                    *close_range.start()
+                };
+                selection.start = buffer.anchor_before(destination);
+                selection.end = selection.start.clone();
+            }
+        }
+
+        self.update_selections(selections, true, ctx);
+    }
+
     fn build_columnar_selection(
         &mut self,
         row: u32,
@@ -1860,6 +1965,7 @@ impl BufferView {
         }
 
         self.add_selections_state = None;
+        self.select_larger_syntax_node_stack.clear();
     }
 
     fn start_transaction(&self, ctx: &mut ViewContext<Self>) {
@@ -1987,7 +2093,7 @@ impl BufferView {
         let ranges = self
             .selections(ctx.as_ref())
             .iter()
-            .map(|s| s.range(buffer).sorted())
+            .map(|s| s.point_range(buffer).sorted())
             .collect();
         self.fold_ranges(ranges, ctx);
     }
@@ -2069,10 +2175,7 @@ impl BufferView {
         let font_size = settings.buffer_font_size;
         let font_id =
             font_cache.select_font(settings.buffer_font_family, &FontProperties::new())?;
-        let digit_count = ((self.buffer.read(app).max_point().row + 1) as f32)
-            .log10()
-            .floor() as usize
-            + 1;
+        let digit_count = (self.buffer.read(app).row_count() as f32).log10().floor() as usize + 1;
 
         Ok(layout_cache
             .layout_str(
@@ -2135,32 +2238,47 @@ impl BufferView {
         }
 
         let settings = self.settings.borrow();
-        let font_id =
-            font_cache.select_font(settings.buffer_font_family, &FontProperties::new())?;
         let font_size = settings.buffer_font_size;
+        let font_family = settings.buffer_font_family;
+        let mut prev_font_properties = FontProperties::new();
+        let mut prev_font_id = font_cache
+            .select_font(font_family, &prev_font_properties)
+            .unwrap();
 
         let mut layouts = Vec::with_capacity(rows.len());
         let mut line = String::new();
+        let mut styles = Vec::new();
         let mut row = rows.start;
-        let snapshot = self.display_map.snapshot(ctx);
-        let chunks = snapshot.chunks_at(DisplayPoint::new(rows.start, 0), ctx);
-        for (chunk_row, chunk_line) in chunks
-            .chain(Some("\n"))
-            .flat_map(|chunk| chunk.split("\n").enumerate())
-        {
-            if chunk_row > 0 {
-                layouts.push(layout_cache.layout_str(
-                    &line,
-                    font_size,
-                    &[(line.len(), font_id, ColorU::black())],
-                ));
-                line.clear();
-                row += 1;
-                if row == rows.end {
-                    break;
+        let mut snapshot = self.display_map.snapshot(ctx);
+        let chunks = snapshot.highlighted_chunks_for_rows(rows.clone());
+        let theme = settings.theme.clone();
+
+        'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", StyleId::default()))) {
+            for (ix, line_chunk) in chunk.split('\n').enumerate() {
+                if ix > 0 {
+                    layouts.push(layout_cache.layout_str(&line, font_size, &styles));
+                    line.clear();
+                    styles.clear();
+                    row += 1;
+                    if row == rows.end {
+                        break 'outer;
+                    }
+                }
+
+                if !line_chunk.is_empty() {
+                    let (color, font_properties) = theme.syntax_style(style_ix);
+                    // Avoid a lookup if the font properties match the previous ones.
+                    let font_id = if font_properties == prev_font_properties {
+                        prev_font_id
+                    } else {
+                        font_cache.select_font(font_family, &font_properties)?
+                    };
+                    line.push_str(line_chunk);
+                    styles.push((line_chunk.len(), font_id, color));
+                    prev_font_id = font_id;
+                    prev_font_properties = font_properties;
                 }
             }
-            line.push_str(chunk_line);
         }
 
         Ok(layouts)
@@ -2246,6 +2364,7 @@ impl BufferView {
             buffer::Event::Saved => ctx.emit(Event::Saved),
             buffer::Event::FileHandleChanged => ctx.emit(Event::FileHandleChanged),
             buffer::Event::Reloaded => ctx.emit(Event::FileHandleChanged),
+            buffer::Event::Reparsed => {}
         }
     }
 }
@@ -2359,7 +2478,12 @@ impl workspace::ItemView for BufferView {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor::Point, settings, test::sample_text};
+    use crate::{
+        editor::Point,
+        settings,
+        test::{build_app_state, sample_text},
+    };
+    use buffer::History;
     use unindent::Unindent;
 
     #[gpui::test]
@@ -3840,6 +3964,146 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_select_larger_smaller_syntax_node(mut app: gpui::TestAppContext) {
+        let app_state = app.read(build_app_state);
+        let lang = app_state.language_registry.select_language("z.rs");
+        let text = r#"
+            use mod1::mod2::{mod3, mod4};
+
+            fn fn_1(param1: bool, param2: &str) {
+                let var1 = "text";
+            }
+        "#
+        .unindent();
+        let buffer = app.add_model(|ctx| {
+            let history = History::new(text.into());
+            Buffer::from_history(0, history, None, lang.cloned(), ctx)
+        });
+        let (_, view) =
+            app.add_window(|ctx| BufferView::for_buffer(buffer, app_state.settings, ctx));
+        view.condition(&app, |view, ctx| !view.buffer.read(ctx).is_parsing())
+            .await;
+
+        view.update(&mut app, |view, ctx| {
+            view.select_display_ranges(
+                &[
+                    DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+                    DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+                    DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+                ],
+                ctx,
+            )
+            .unwrap();
+            view.select_larger_syntax_node(&(), ctx);
+        });
+        assert_eq!(
+            view.read_with(&app, |view, ctx| view.selection_ranges(ctx)),
+            &[
+                DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
+                DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+                DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
+            ]
+        );
+
+        view.update(&mut app, |view, ctx| {
+            view.select_larger_syntax_node(&(), ctx);
+        });
+        assert_eq!(
+            view.read_with(&app, |view, ctx| view.selection_ranges(ctx)),
+            &[
+                DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+                DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
+            ]
+        );
+
+        view.update(&mut app, |view, ctx| {
+            view.select_larger_syntax_node(&(), ctx);
+        });
+        assert_eq!(
+            view.read_with(&app, |view, ctx| view.selection_ranges(ctx)),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(5, 0)]
+        );
+
+        // Trying to expand the selected syntax node one more time has no effect.
+        view.update(&mut app, |view, ctx| {
+            view.select_larger_syntax_node(&(), ctx);
+        });
+        assert_eq!(
+            view.read_with(&app, |view, ctx| view.selection_ranges(ctx)),
+            &[DisplayPoint::new(0, 0)..DisplayPoint::new(5, 0)]
+        );
+
+        view.update(&mut app, |view, ctx| {
+            view.select_smaller_syntax_node(&(), ctx);
+        });
+        assert_eq!(
+            view.read_with(&app, |view, ctx| view.selection_ranges(ctx)),
+            &[
+                DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+                DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
+            ]
+        );
+
+        view.update(&mut app, |view, ctx| {
+            view.select_smaller_syntax_node(&(), ctx);
+        });
+        assert_eq!(
+            view.read_with(&app, |view, ctx| view.selection_ranges(ctx)),
+            &[
+                DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
+                DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+                DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
+            ]
+        );
+
+        view.update(&mut app, |view, ctx| {
+            view.select_smaller_syntax_node(&(), ctx);
+        });
+        assert_eq!(
+            view.read_with(&app, |view, ctx| view.selection_ranges(ctx)),
+            &[
+                DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+                DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+                DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+            ]
+        );
+
+        // Trying to shrink the selected syntax node one more time has no effect.
+        view.update(&mut app, |view, ctx| {
+            view.select_smaller_syntax_node(&(), ctx);
+        });
+        assert_eq!(
+            view.read_with(&app, |view, ctx| view.selection_ranges(ctx)),
+            &[
+                DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+                DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+                DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+            ]
+        );
+
+        // Ensure that we keep expanding the selection if the larger selection starts or ends within
+        // a fold.
+        view.update(&mut app, |view, ctx| {
+            view.fold_ranges(
+                vec![
+                    Point::new(0, 21)..Point::new(0, 24),
+                    Point::new(3, 20)..Point::new(3, 22),
+                ],
+                ctx,
+            );
+            view.select_larger_syntax_node(&(), ctx);
+        });
+        assert_eq!(
+            view.read_with(&app, |view, ctx| view.selection_ranges(ctx)),
+            &[
+                DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+                DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+                DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
+            ]
+        );
+    }
+
     impl BufferView {
         fn selection_ranges(&self, app: &AppContext) -> Vec<Range<DisplayPoint>> {
             self.selections_in_range(DisplayPoint::zero()..self.max_point(app), app)

zed/src/editor/display_map/fold_map.rs šŸ”—

@@ -4,6 +4,7 @@ use super::{
 };
 use crate::{
     editor::buffer,
+    settings::StyleId,
     sum_tree::{self, Cursor, FilterCursor, SeekBias, SumTree},
     time,
 };
@@ -45,7 +46,7 @@ impl FoldMap {
     pub fn snapshot(&self, ctx: &AppContext) -> FoldMapSnapshot {
         FoldMapSnapshot {
             transforms: self.sync(ctx).clone(),
-            buffer: self.buffer.clone(),
+            buffer: self.buffer.read(ctx).snapshot(),
         }
     }
 
@@ -100,6 +101,7 @@ impl FoldMap {
                 edits.push(Edit {
                     old_range: range.clone(),
                     new_range: range.clone(),
+                    ..Default::default()
                 });
             }
         }
@@ -144,6 +146,7 @@ impl FoldMap {
                 edits.push(Edit {
                     old_range: offset_range.clone(),
                     new_range: offset_range,
+                    ..Default::default()
                 });
                 fold_ixs_to_delete.push(*folds_cursor.start());
                 folds_cursor.next();
@@ -189,6 +192,18 @@ impl FoldMap {
         })
     }
 
+    pub fn intersects_fold<T>(&self, offset: T, ctx: &AppContext) -> bool
+    where
+        T: ToOffset,
+    {
+        let buffer = self.buffer.read(ctx);
+        let offset = offset.to_offset(buffer);
+        let transforms = self.sync(ctx);
+        let mut cursor = transforms.cursor::<usize, usize>();
+        cursor.seek(&offset, SeekBias::Right, &());
+        cursor.item().map_or(false, |t| t.display_text.is_some())
+    }
+
     pub fn is_line_folded(&self, display_row: u32, ctx: &AppContext) -> bool {
         let transforms = self.sync(ctx);
         let mut cursor = transforms.cursor::<DisplayPoint, DisplayPoint>();
@@ -207,11 +222,11 @@ impl FoldMap {
     }
 
     pub fn to_buffer_offset(&self, point: DisplayPoint, ctx: &AppContext) -> usize {
-        self.snapshot(ctx).to_buffer_offset(point, ctx)
+        self.snapshot(ctx).to_buffer_offset(point)
     }
 
     pub fn to_display_offset(&self, point: DisplayPoint, ctx: &AppContext) -> DisplayOffset {
-        self.snapshot(ctx).to_display_offset(point, ctx)
+        self.snapshot(ctx).to_display_offset(point)
     }
 
     pub fn to_buffer_point(&self, display_point: DisplayPoint, ctx: &AppContext) -> Point {
@@ -391,7 +406,7 @@ impl FoldMap {
 
 pub struct FoldMapSnapshot {
     transforms: SumTree<Transform>,
-    buffer: ModelHandle<Buffer>,
+    buffer: buffer::Snapshot,
 }
 
 impl FoldMapSnapshot {
@@ -410,30 +425,49 @@ impl FoldMapSnapshot {
         }
     }
 
-    pub fn chunks_at<'a>(&'a self, offset: DisplayOffset, ctx: &'a AppContext) -> Chunks<'a> {
+    pub fn max_point(&self) -> DisplayPoint {
+        DisplayPoint(self.transforms.summary().display.lines)
+    }
+
+    pub fn chunks_at(&self, offset: DisplayOffset) -> Chunks {
         let mut transform_cursor = self.transforms.cursor::<DisplayOffset, TransformSummary>();
         transform_cursor.seek(&offset, SeekBias::Right, &());
         let overshoot = offset.0 - transform_cursor.start().display.bytes;
         let buffer_offset = transform_cursor.start().buffer.bytes + overshoot;
-        let buffer = self.buffer.read(ctx);
-        let rope_cursor = buffer.text_for_range(buffer_offset..buffer.len());
         Chunks {
             transform_cursor,
             buffer_offset,
-            buffer_chunks: rope_cursor,
+            buffer_chunks: self.buffer.text_for_range(buffer_offset..self.buffer.len()),
         }
     }
 
-    pub fn chars_at<'a>(
-        &'a self,
-        point: DisplayPoint,
-        ctx: &'a AppContext,
-    ) -> impl Iterator<Item = char> + 'a {
-        let offset = self.to_display_offset(point, ctx);
-        self.chunks_at(offset, ctx).flat_map(str::chars)
+    pub fn highlighted_chunks(&mut self, range: Range<DisplayOffset>) -> HighlightedChunks {
+        let mut transform_cursor = self.transforms.cursor::<DisplayOffset, TransformSummary>();
+
+        transform_cursor.seek(&range.end, SeekBias::Right, &());
+        let overshoot = range.end.0 - transform_cursor.start().display.bytes;
+        let buffer_end = transform_cursor.start().buffer.bytes + overshoot;
+
+        transform_cursor.seek(&range.start, SeekBias::Right, &());
+        let overshoot = range.start.0 - transform_cursor.start().display.bytes;
+        let buffer_start = transform_cursor.start().buffer.bytes + overshoot;
+
+        HighlightedChunks {
+            transform_cursor,
+            buffer_offset: buffer_start,
+            buffer_chunks: self
+                .buffer
+                .highlighted_text_for_range(buffer_start..buffer_end),
+            buffer_chunk: None,
+        }
     }
 
-    pub fn to_display_offset(&self, point: DisplayPoint, ctx: &AppContext) -> DisplayOffset {
+    pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
+        let offset = self.to_display_offset(point);
+        self.chunks_at(offset).flat_map(str::chars)
+    }
+
+    pub fn to_display_offset(&self, point: DisplayPoint) -> DisplayOffset {
         let mut cursor = self.transforms.cursor::<DisplayPoint, TransformSummary>();
         cursor.seek(&point, SeekBias::Right, &());
         let overshoot = point.0 - cursor.start().display.lines;
@@ -441,27 +475,24 @@ impl FoldMapSnapshot {
         if !overshoot.is_zero() {
             let transform = cursor.item().expect("display point out of range");
             assert!(transform.display_text.is_none());
-            let end_buffer_offset =
-                (cursor.start().buffer.lines + overshoot).to_offset(self.buffer.read(ctx));
+            let end_buffer_offset = self
+                .buffer
+                .to_offset(cursor.start().buffer.lines + overshoot);
             offset += end_buffer_offset - cursor.start().buffer.bytes;
         }
         DisplayOffset(offset)
     }
 
-    pub fn to_buffer_offset(&self, point: DisplayPoint, ctx: &AppContext) -> usize {
+    pub fn to_buffer_offset(&self, point: DisplayPoint) -> usize {
         let mut cursor = self.transforms.cursor::<DisplayPoint, TransformSummary>();
         cursor.seek(&point, SeekBias::Right, &());
         let overshoot = point.0 - cursor.start().display.lines;
-        (cursor.start().buffer.lines + overshoot).to_offset(self.buffer.read(ctx))
+        self.buffer
+            .to_offset(cursor.start().buffer.lines + overshoot)
     }
 
     #[cfg(test)]
-    pub fn clip_offset(
-        &self,
-        offset: DisplayOffset,
-        bias: Bias,
-        ctx: &AppContext,
-    ) -> DisplayOffset {
+    pub fn clip_offset(&self, offset: DisplayOffset, bias: Bias) -> DisplayOffset {
         let mut cursor = self.transforms.cursor::<DisplayOffset, TransformSummary>();
         cursor.seek(&offset, SeekBias::Right, &());
         if let Some(transform) = cursor.item() {
@@ -475,7 +506,7 @@ impl FoldMapSnapshot {
             } else {
                 let overshoot = offset.0 - transform_start;
                 let buffer_offset = cursor.start().buffer.bytes + overshoot;
-                let clipped_buffer_offset = self.buffer.read(ctx).clip_offset(buffer_offset, bias);
+                let clipped_buffer_offset = self.buffer.clip_offset(buffer_offset, bias);
                 DisplayOffset(
                     (offset.0 as isize + (clipped_buffer_offset as isize - buffer_offset as isize))
                         as usize,
@@ -486,7 +517,7 @@ impl FoldMapSnapshot {
         }
     }
 
-    pub fn clip_point(&self, point: DisplayPoint, bias: Bias, ctx: &AppContext) -> DisplayPoint {
+    pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
         let mut cursor = self.transforms.cursor::<DisplayPoint, TransformSummary>();
         cursor.seek(&point, SeekBias::Right, &());
         if let Some(transform) = cursor.item() {
@@ -500,8 +531,7 @@ impl FoldMapSnapshot {
             } else {
                 let overshoot = point.0 - transform_start;
                 let buffer_position = cursor.start().buffer.lines + overshoot;
-                let clipped_buffer_position =
-                    self.buffer.read(ctx).clip_point(buffer_position, bias);
+                let clipped_buffer_position = self.buffer.clip_point(buffer_position, bias);
                 DisplayPoint::new(
                     point.row(),
                     ((point.column() as i32) + clipped_buffer_position.column as i32
@@ -667,7 +697,7 @@ impl<'a> Iterator for BufferRows<'a> {
 
 pub struct Chunks<'a> {
     transform_cursor: Cursor<'a, Transform, DisplayOffset, TransformSummary>,
-    buffer_chunks: buffer::ChunksIter<'a>,
+    buffer_chunks: buffer::Chunks<'a>,
     buffer_offset: usize,
 }
 
@@ -685,7 +715,7 @@ impl<'a> Iterator for Chunks<'a> {
         // advance the transform and buffer cursors to the end of the fold.
         if let Some(display_text) = transform.display_text {
             self.buffer_offset += transform.summary.buffer.bytes;
-            self.buffer_chunks.advance_to(self.buffer_offset);
+            self.buffer_chunks.seek(self.buffer_offset);
 
             while self.buffer_offset >= self.transform_cursor.end().buffer.bytes
                 && self.transform_cursor.item().is_some()
@@ -718,6 +748,70 @@ impl<'a> Iterator for Chunks<'a> {
     }
 }
 
+pub struct HighlightedChunks<'a> {
+    transform_cursor: Cursor<'a, Transform, DisplayOffset, TransformSummary>,
+    buffer_chunks: buffer::HighlightedChunks<'a>,
+    buffer_chunk: Option<(usize, &'a str, StyleId)>,
+    buffer_offset: usize,
+}
+
+impl<'a> Iterator for HighlightedChunks<'a> {
+    type Item = (&'a str, StyleId);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let transform = if let Some(item) = self.transform_cursor.item() {
+            item
+        } else {
+            return None;
+        };
+
+        // If we're in a fold, then return the fold's display text and
+        // advance the transform and buffer cursors to the end of the fold.
+        if let Some(display_text) = transform.display_text {
+            self.buffer_chunk.take();
+            self.buffer_offset += transform.summary.buffer.bytes;
+            self.buffer_chunks.seek(self.buffer_offset);
+
+            while self.buffer_offset >= self.transform_cursor.end().buffer.bytes
+                && self.transform_cursor.item().is_some()
+            {
+                self.transform_cursor.next();
+            }
+
+            return Some((display_text, StyleId::default()));
+        }
+
+        // Retrieve a chunk from the current location in the buffer.
+        if self.buffer_chunk.is_none() {
+            let chunk_offset = self.buffer_chunks.offset();
+            self.buffer_chunk = self
+                .buffer_chunks
+                .next()
+                .map(|(chunk, capture_ix)| (chunk_offset, chunk, capture_ix));
+        }
+
+        // Otherwise, take a chunk from the buffer's text.
+        if let Some((chunk_offset, mut chunk, capture_ix)) = self.buffer_chunk {
+            let offset_in_chunk = self.buffer_offset - chunk_offset;
+            chunk = &chunk[offset_in_chunk..];
+
+            // Truncate the chunk so that it ends at the next fold.
+            let region_end = self.transform_cursor.end().buffer.bytes - self.buffer_offset;
+            if chunk.len() >= region_end {
+                chunk = &chunk[0..region_end];
+                self.transform_cursor.next();
+            } else {
+                self.buffer_chunk.take();
+            }
+
+            self.buffer_offset += chunk.len();
+            return Some((chunk, capture_ix));
+        }
+
+        None
+    }
+}
+
 impl<'a> sum_tree::Dimension<'a, TransformSummary> for DisplayPoint {
     fn add_summary(&mut self, summary: &'a TransformSummary) {
         self.0 += &summary.display.lines;
@@ -1046,11 +1140,10 @@ mod tests {
                     let offset = map.snapshot(app.as_ref()).clip_offset(
                         DisplayOffset(rng.gen_range(0..=map.len(app.as_ref()))),
                         Bias::Right,
-                        app.as_ref(),
                     );
                     assert_eq!(
                         map.snapshot(app.as_ref())
-                            .chunks_at(offset, app.as_ref())
+                            .chunks_at(offset)
                             .collect::<String>(),
                         &expected_text[offset.0..],
                     );
@@ -1133,9 +1226,7 @@ mod tests {
 
     impl FoldMap {
         fn text(&self, app: &AppContext) -> String {
-            self.snapshot(app)
-                .chunks_at(DisplayOffset(0), app)
-                .collect()
+            self.snapshot(app).chunks_at(DisplayOffset(0)).collect()
         }
 
         fn merged_fold_ranges(&self, app: &AppContext) -> Vec<Range<usize>> {

zed/src/editor/display_map/mod.rs šŸ”—

@@ -1,10 +1,12 @@
 mod fold_map;
 
+use crate::settings::StyleId;
+
 use super::{buffer, Anchor, Bias, Buffer, Edit, Point, ToOffset, ToPoint};
 pub use fold_map::BufferRows;
 use fold_map::{FoldMap, FoldMapSnapshot};
 use gpui::{AppContext, ModelHandle};
-use std::ops::Range;
+use std::{mem, ops::Range};
 
 pub struct DisplayMap {
     buffer: ModelHandle<Buffer>,
@@ -55,21 +57,23 @@ impl DisplayMap {
         self.fold_map.unfold(ranges, ctx)
     }
 
+    pub fn intersects_fold<T: ToOffset>(&self, offset: T, ctx: &AppContext) -> bool {
+        self.fold_map.intersects_fold(offset, ctx)
+    }
+
     pub fn is_line_folded(&self, display_row: u32, ctx: &AppContext) -> bool {
         self.fold_map.is_line_folded(display_row, ctx)
     }
 
     pub fn text(&self, ctx: &AppContext) -> String {
-        self.snapshot(ctx)
-            .chunks_at(DisplayPoint::zero(), ctx)
-            .collect()
+        self.snapshot(ctx).chunks_at(DisplayPoint::zero()).collect()
     }
 
     pub fn line(&self, display_row: u32, ctx: &AppContext) -> String {
         let mut result = String::new();
         for chunk in self
             .snapshot(ctx)
-            .chunks_at(DisplayPoint::new(display_row, 0), ctx)
+            .chunks_at(DisplayPoint::new(display_row, 0))
         {
             if let Some(ix) = chunk.find('\n') {
                 result.push_str(&chunk[0..ix]);
@@ -86,7 +90,7 @@ impl DisplayMap {
         let mut is_blank = true;
         for c in self
             .snapshot(ctx)
-            .chars_at(DisplayPoint::new(display_row, 0), ctx)
+            .chars_at(DisplayPoint::new(display_row, 0))
         {
             if c == ' ' {
                 indent += 1;
@@ -104,9 +108,8 @@ impl DisplayMap {
             .column()
     }
 
-    // TODO - make this delegate to the DisplayMapSnapshot
     pub fn max_point(&self, ctx: &AppContext) -> DisplayPoint {
-        self.fold_map.max_point(ctx).expand_tabs(self, ctx)
+        self.snapshot(ctx).max_point().expand_tabs(self, ctx)
     }
 
     pub fn longest_row(&self, ctx: &AppContext) -> u32 {
@@ -136,12 +139,15 @@ impl DisplayMapSnapshot {
         self.folds_snapshot.buffer_rows(start_row)
     }
 
-    pub fn chunks_at<'a>(&'a self, point: DisplayPoint, app: &'a AppContext) -> Chunks<'a> {
-        let (point, expanded_char_column, to_next_stop) =
-            self.collapse_tabs(point, Bias::Left, app);
+    pub fn max_point(&self) -> DisplayPoint {
+        self.expand_tabs(self.folds_snapshot.max_point())
+    }
+
+    pub fn chunks_at(&self, point: DisplayPoint) -> Chunks {
+        let (point, expanded_char_column, to_next_stop) = self.collapse_tabs(point, Bias::Left);
         let fold_chunks = self
             .folds_snapshot
-            .chunks_at(self.folds_snapshot.to_display_offset(point, app), app);
+            .chunks_at(self.folds_snapshot.to_display_offset(point));
         Chunks {
             fold_chunks,
             column: expanded_char_column,
@@ -151,18 +157,28 @@ impl DisplayMapSnapshot {
         }
     }
 
-    pub fn chars_at<'a>(
-        &'a self,
-        point: DisplayPoint,
-        app: &'a AppContext,
-    ) -> impl Iterator<Item = char> + 'a {
-        self.chunks_at(point, app).flat_map(str::chars)
+    pub fn highlighted_chunks_for_rows(&mut self, rows: Range<u32>) -> HighlightedChunks {
+        let start = DisplayPoint::new(rows.start, 0);
+        let start = self.folds_snapshot.to_display_offset(start);
+        let end = DisplayPoint::new(rows.end, 0).min(self.max_point());
+        let end = self.folds_snapshot.to_display_offset(end);
+        HighlightedChunks {
+            fold_chunks: self.folds_snapshot.highlighted_chunks(start..end),
+            column: 0,
+            tab_size: self.tab_size,
+            chunk: "",
+            style_id: Default::default(),
+        }
     }
 
-    pub fn column_to_chars(&self, display_row: u32, target: u32, ctx: &AppContext) -> u32 {
+    pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
+        self.chunks_at(point).flat_map(str::chars)
+    }
+
+    pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
         let mut count = 0;
         let mut column = 0;
-        for c in self.chars_at(DisplayPoint::new(display_row, 0), ctx) {
+        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
             if column >= target {
                 break;
             }
@@ -172,10 +188,10 @@ impl DisplayMapSnapshot {
         count
     }
 
-    pub fn column_from_chars(&self, display_row: u32, char_count: u32, ctx: &AppContext) -> u32 {
+    pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
         let mut count = 0;
         let mut column = 0;
-        for c in self.chars_at(DisplayPoint::new(display_row, 0), ctx) {
+        for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
             if c == '\n' || count >= char_count {
                 break;
             }
@@ -185,32 +201,26 @@ impl DisplayMapSnapshot {
         column
     }
 
-    pub fn clip_point(&self, point: DisplayPoint, bias: Bias, ctx: &AppContext) -> DisplayPoint {
+    pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
         self.expand_tabs(
             self.folds_snapshot
-                .clip_point(self.collapse_tabs(point, bias, ctx).0, bias, ctx),
-            ctx,
+                .clip_point(self.collapse_tabs(point, bias).0, bias),
         )
     }
 
-    fn expand_tabs(&self, mut point: DisplayPoint, ctx: &AppContext) -> DisplayPoint {
+    fn expand_tabs(&self, mut point: DisplayPoint) -> DisplayPoint {
         let chars = self
             .folds_snapshot
-            .chars_at(DisplayPoint(Point::new(point.row(), 0)), ctx);
+            .chars_at(DisplayPoint(Point::new(point.row(), 0)));
         let expanded = expand_tabs(chars, point.column() as usize, self.tab_size);
         *point.column_mut() = expanded as u32;
         point
     }
 
-    fn collapse_tabs(
-        &self,
-        mut point: DisplayPoint,
-        bias: Bias,
-        ctx: &AppContext,
-    ) -> (DisplayPoint, usize, usize) {
+    fn collapse_tabs(&self, mut point: DisplayPoint, bias: Bias) -> (DisplayPoint, usize, usize) {
         let chars = self
             .folds_snapshot
-            .chars_at(DisplayPoint(Point::new(point.row(), 0)), ctx);
+            .chars_at(DisplayPoint(Point::new(point.row(), 0)));
         let expanded = point.column() as usize;
         let (collapsed, expanded_char_column, to_next_stop) =
             collapse_tabs(chars, expanded, bias, self.tab_size);
@@ -258,11 +268,11 @@ impl DisplayPoint {
     }
 
     fn expand_tabs(self, map: &DisplayMap, ctx: &AppContext) -> Self {
-        map.snapshot(ctx).expand_tabs(self, ctx)
+        map.snapshot(ctx).expand_tabs(self)
     }
 
     fn collapse_tabs(self, map: &DisplayMap, bias: Bias, ctx: &AppContext) -> Self {
-        map.snapshot(ctx).collapse_tabs(self, bias, ctx).0
+        map.snapshot(ctx).collapse_tabs(self, bias).0
     }
 }
 
@@ -270,7 +280,7 @@ impl Point {
     pub fn to_display_point(self, map: &DisplayMap, ctx: &AppContext) -> DisplayPoint {
         let mut display_point = map.fold_map.to_display_point(self, ctx);
         let snapshot = map.fold_map.snapshot(ctx);
-        let chars = snapshot.chars_at(DisplayPoint::new(display_point.row(), 0), ctx);
+        let chars = snapshot.chars_at(DisplayPoint::new(display_point.row(), 0));
         *display_point.column_mut() =
             expand_tabs(chars, display_point.column() as usize, map.tab_size) as u32;
         display_point
@@ -336,6 +346,50 @@ impl<'a> Iterator for Chunks<'a> {
     }
 }
 
+pub struct HighlightedChunks<'a> {
+    fold_chunks: fold_map::HighlightedChunks<'a>,
+    chunk: &'a str,
+    style_id: StyleId,
+    column: usize,
+    tab_size: usize,
+}
+
+impl<'a> Iterator for HighlightedChunks<'a> {
+    type Item = (&'a str, StyleId);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.chunk.is_empty() {
+            if let Some((chunk, style_id)) = self.fold_chunks.next() {
+                self.chunk = chunk;
+                self.style_id = style_id;
+            } else {
+                return None;
+            }
+        }
+
+        for (ix, c) in self.chunk.char_indices() {
+            match c {
+                '\t' => {
+                    if ix > 0 {
+                        let (prefix, suffix) = self.chunk.split_at(ix);
+                        self.chunk = suffix;
+                        return Some((prefix, self.style_id));
+                    } else {
+                        self.chunk = &self.chunk[1..];
+                        let len = self.tab_size - self.column % self.tab_size;
+                        self.column += len;
+                        return Some((&SPACES[0..len], self.style_id));
+                    }
+                }
+                '\n' => self.column = 0,
+                _ => self.column += 1,
+            }
+        }
+
+        Some((mem::take(&mut self.chunk), mem::take(&mut self.style_id)))
+    }
+}
+
 pub fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
     let mut expanded_chars = 0;
     let mut expanded_bytes = 0;
@@ -400,7 +454,13 @@ pub fn collapse_tabs(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::test::*;
+    use crate::{
+        language::{Language, LanguageConfig},
+        settings::Theme,
+        test::*,
+    };
+    use buffer::History;
+    use std::sync::Arc;
 
     #[gpui::test]
     fn test_chunks_at(app: &mut gpui::MutableAppContext) {
@@ -423,24 +483,125 @@ mod tests {
 
         assert_eq!(
             &map.snapshot(app.as_ref())
-                .chunks_at(DisplayPoint::new(1, 0), app.as_ref())
+                .chunks_at(DisplayPoint::new(1, 0))
                 .collect::<String>()[0..10],
             "    b   bb"
         );
         assert_eq!(
             &map.snapshot(app.as_ref())
-                .chunks_at(DisplayPoint::new(1, 2), app.as_ref())
+                .chunks_at(DisplayPoint::new(1, 2))
                 .collect::<String>()[0..10],
             "  b   bbbb"
         );
         assert_eq!(
             &map.snapshot(app.as_ref())
-                .chunks_at(DisplayPoint::new(1, 6), app.as_ref())
+                .chunks_at(DisplayPoint::new(1, 6))
                 .collect::<String>()[0..13],
             "  bbbbb\nc   c"
         );
     }
 
+    #[gpui::test]
+    async fn test_highlighted_chunks_at(mut app: gpui::TestAppContext) {
+        use unindent::Unindent as _;
+
+        let grammar = tree_sitter_rust::language();
+        let text = r#"
+            fn outer() {}
+
+            mod module {
+                fn inner() {}
+            }"#
+        .unindent();
+        let highlight_query = tree_sitter::Query::new(
+            grammar,
+            r#"
+            (mod_item name: (identifier) body: _ @mod.body)
+            (function_item name: (identifier) @fn.name)"#,
+        )
+        .unwrap();
+        let theme = Theme::parse(
+            r#"
+            [syntax]
+            "mod.body" = 0xff0000
+            "fn.name" = 0x00ff00"#,
+        )
+        .unwrap();
+        let lang = Arc::new(Language {
+            config: LanguageConfig {
+                name: "Test".to_string(),
+                path_suffixes: vec![".test".to_string()],
+                ..Default::default()
+            },
+            grammar: grammar.clone(),
+            highlight_query,
+            brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
+            theme_mapping: Default::default(),
+        });
+        lang.set_theme(&theme);
+
+        let buffer = app.add_model(|ctx| {
+            Buffer::from_history(0, History::new(text.into()), None, Some(lang), ctx)
+        });
+        buffer.condition(&app, |buf, _| !buf.is_parsing()).await;
+
+        let mut map = app.read(|ctx| DisplayMap::new(buffer, 2, ctx));
+        assert_eq!(
+            app.read(|ctx| highlighted_chunks(0..5, &map, &theme, ctx)),
+            vec![
+                ("fn ".to_string(), None),
+                ("outer".to_string(), Some("fn.name")),
+                ("() {}\n\nmod module ".to_string(), None),
+                ("{\n    fn ".to_string(), Some("mod.body")),
+                ("inner".to_string(), Some("fn.name")),
+                ("() {}\n}".to_string(), Some("mod.body")),
+            ]
+        );
+        assert_eq!(
+            app.read(|ctx| highlighted_chunks(3..5, &map, &theme, ctx)),
+            vec![
+                ("    fn ".to_string(), Some("mod.body")),
+                ("inner".to_string(), Some("fn.name")),
+                ("() {}\n}".to_string(), Some("mod.body")),
+            ]
+        );
+
+        app.read(|ctx| map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], ctx));
+        assert_eq!(
+            app.read(|ctx| highlighted_chunks(0..2, &map, &theme, ctx)),
+            vec![
+                ("fn ".to_string(), None),
+                ("out".to_string(), Some("fn.name")),
+                ("…".to_string(), None),
+                ("  fn ".to_string(), Some("mod.body")),
+                ("inner".to_string(), Some("fn.name")),
+                ("() {}\n}".to_string(), Some("mod.body")),
+            ]
+        );
+
+        fn highlighted_chunks<'a>(
+            rows: Range<u32>,
+            map: &DisplayMap,
+            theme: &'a Theme,
+            ctx: &AppContext,
+        ) -> Vec<(String, Option<&'a str>)> {
+            let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
+            for (chunk, style_id) in map.snapshot(ctx).highlighted_chunks_for_rows(rows) {
+                let style_name = theme.syntax_style_name(style_id);
+                if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
+                    if style_name == *last_style_name {
+                        last_chunk.push_str(chunk);
+                    } else {
+                        chunks.push((chunk.to_string(), style_name));
+                    }
+                } else {
+                    chunks.push((chunk.to_string(), style_name));
+                }
+            }
+            chunks
+        }
+    }
+
     #[gpui::test]
     fn test_clip_point(app: &mut gpui::MutableAppContext) {
         use Bias::{Left, Right};
@@ -470,7 +631,7 @@ mod tests {
             ),
         ] {
             assert_eq!(
-                map.clip_point(DisplayPoint::new(1, input_column as u32), bias, ctx),
+                map.clip_point(DisplayPoint::new(1, input_column as u32), bias),
                 DisplayPoint::new(1, output_column as u32),
                 "clip_point(({}, {}))",
                 1,
@@ -520,7 +681,7 @@ mod tests {
         );
         assert_eq!(
             map.snapshot(ctx)
-                .chunks_at(DisplayPoint::new(0, "āœ…      ".len() as u32), ctx)
+                .chunks_at(DisplayPoint::new(0, "āœ…      ".len() as u32))
                 .collect::<String>(),
             " α\nβ   \nšŸ€Ī²      γ"
         );
@@ -534,26 +695,20 @@ mod tests {
         );
         assert_eq!(
             map.snapshot(ctx)
-                .chunks_at(DisplayPoint::new(0, "āœ… ".len() as u32), ctx)
+                .chunks_at(DisplayPoint::new(0, "āœ… ".len() as u32))
                 .collect::<String>(),
             "      α\nβ   \nšŸ€Ī²      γ"
         );
 
         // Clipping display points inside of multi-byte characters
         assert_eq!(
-            map.snapshot(ctx).clip_point(
-                DisplayPoint::new(0, "āœ…".len() as u32 - 1),
-                Bias::Left,
-                ctx
-            ),
+            map.snapshot(ctx)
+                .clip_point(DisplayPoint::new(0, "āœ…".len() as u32 - 1), Bias::Left),
             DisplayPoint::new(0, 0)
         );
         assert_eq!(
-            map.snapshot(ctx).clip_point(
-                DisplayPoint::new(0, "āœ…".len() as u32 - 1),
-                Bias::Right,
-                ctx
-            ),
+            map.snapshot(ctx)
+                .clip_point(DisplayPoint::new(0, "āœ…".len() as u32 - 1), Bias::Right),
             DisplayPoint::new(0, "āœ…".len() as u32)
         );
     }

zed/src/editor/mod.rs šŸ”—

@@ -9,7 +9,10 @@ pub use buffer_element::*;
 pub use buffer_view::*;
 pub use display_map::DisplayPoint;
 use display_map::*;
-use std::{cmp, ops::Range};
+use std::{
+    cmp,
+    ops::{Range, RangeInclusive},
+};
 
 #[derive(Copy, Clone)]
 pub enum Bias {
@@ -19,10 +22,15 @@ pub enum Bias {
 
 trait RangeExt<T> {
     fn sorted(&self) -> Range<T>;
+    fn to_inclusive(&self) -> RangeInclusive<T>;
 }
 
 impl<T: Ord + Clone> RangeExt<T> for Range<T> {
     fn sorted(&self) -> Self {
         cmp::min(&self.start, &self.end).clone()..cmp::max(&self.start, &self.end).clone()
     }
+
+    fn to_inclusive(&self) -> RangeInclusive<T> {
+        self.start.clone()..=self.end.clone()
+    }
 }

zed/src/editor/movement.rs šŸ”—

@@ -9,7 +9,7 @@ pub fn left(map: &DisplayMap, mut point: DisplayPoint, app: &AppContext) -> Resu
         *point.row_mut() -= 1;
         *point.column_mut() = map.line_len(point.row(), app);
     }
-    Ok(map.snapshot(app).clip_point(point, Bias::Left, app))
+    Ok(map.snapshot(app).clip_point(point, Bias::Left))
 }
 
 pub fn right(map: &DisplayMap, mut point: DisplayPoint, app: &AppContext) -> Result<DisplayPoint> {
@@ -20,7 +20,7 @@ pub fn right(map: &DisplayMap, mut point: DisplayPoint, app: &AppContext) -> Res
         *point.row_mut() += 1;
         *point.column_mut() = 0;
     }
-    Ok(map.snapshot(app).clip_point(point, Bias::Right, app))
+    Ok(map.snapshot(app).clip_point(point, Bias::Right))
 }
 
 pub fn up(
@@ -33,12 +33,12 @@ pub fn up(
     let goal_column = if let SelectionGoal::Column(column) = goal {
         column
     } else {
-        map.column_to_chars(point.row(), point.column(), app)
+        map.column_to_chars(point.row(), point.column())
     };
 
     if point.row() > 0 {
         *point.row_mut() -= 1;
-        *point.column_mut() = map.column_from_chars(point.row(), goal_column, app);
+        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
     } else {
         point = DisplayPoint::new(0, 0);
     }
@@ -57,12 +57,12 @@ pub fn down(
     let goal_column = if let SelectionGoal::Column(column) = goal {
         column
     } else {
-        map.column_to_chars(point.row(), point.column(), app)
+        map.column_to_chars(point.row(), point.column())
     };
 
     if point.row() < max_point.row() {
         *point.row_mut() += 1;
-        *point.column_mut() = map.column_from_chars(point.row(), goal_column, app);
+        *point.column_mut() = map.column_from_chars(point.row(), goal_column);
     } else {
         point = max_point;
     }
@@ -107,7 +107,7 @@ pub fn prev_word_boundary(
         let mut boundary = DisplayPoint::new(point.row(), 0);
         let mut column = 0;
         let mut prev_c = None;
-        for c in map.snapshot(app).chars_at(boundary, app) {
+        for c in map.snapshot(app).chars_at(boundary) {
             if column >= point.column() {
                 break;
             }
@@ -129,7 +129,7 @@ pub fn next_word_boundary(
     app: &AppContext,
 ) -> Result<DisplayPoint> {
     let mut prev_c = None;
-    for c in map.snapshot(app).chars_at(point, app) {
+    for c in map.snapshot(app).chars_at(point) {
         if prev_c.is_some() && (c == '\n' || char_kind(prev_c.unwrap()) != char_kind(c)) {
             break;
         }

zed/src/file_finder.rs šŸ”—

@@ -458,7 +458,11 @@ impl FileFinder {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor, settings, test::temp_tree, workspace::Workspace};
+    use crate::{
+        editor,
+        test::{build_app_state, temp_tree},
+        workspace::Workspace,
+    };
     use serde_json::json;
     use std::fs;
     use tempdir::TempDir;
@@ -474,9 +478,10 @@ mod tests {
             editor::init(ctx);
         });
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (window_id, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(tmp_dir.path(), ctx);
             workspace
         });
@@ -541,15 +546,21 @@ mod tests {
             "hi": "",
             "hiccup": "",
         }));
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings.clone(), ctx);
+            let mut workspace = Workspace::new(
+                0,
+                app_state.settings.clone(),
+                app_state.language_registry.clone(),
+                ctx,
+            );
             workspace.add_worktree(tmp_dir.path(), ctx);
             workspace
         });
         app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
             .await;
-        let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
+        let (_, finder) =
+            app.add_window(|ctx| FileFinder::new(app_state.settings, workspace.clone(), ctx));
 
         let query = "hi".to_string();
         finder
@@ -598,15 +609,21 @@ mod tests {
         fs::create_dir(&dir_path).unwrap();
         fs::write(&file_path, "").unwrap();
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings.clone(), ctx);
+            let mut workspace = Workspace::new(
+                0,
+                app_state.settings.clone(),
+                app_state.language_registry.clone(),
+                ctx,
+            );
             workspace.add_worktree(&file_path, ctx);
             workspace
         });
         app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
             .await;
-        let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
+        let (_, finder) =
+            app.add_window(|ctx| FileFinder::new(app_state.settings, workspace.clone(), ctx));
 
         // Even though there is only one worktree, that worktree's filename
         // is included in the matching, because the worktree is a single file.
@@ -641,9 +658,17 @@ mod tests {
             "dir1": { "a.txt": "" },
             "dir2": { "a.txt": "" }
         }));
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
 
-        let (_, workspace) = app.add_window(|ctx| Workspace::new(0, settings.clone(), ctx));
+        let app_state = app.read(build_app_state);
+
+        let (_, workspace) = app.add_window(|ctx| {
+            Workspace::new(
+                0,
+                app_state.settings.clone(),
+                app_state.language_registry.clone(),
+                ctx,
+            )
+        });
 
         workspace
             .update(&mut app, |workspace, ctx| {
@@ -656,7 +681,8 @@ mod tests {
         app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
             .await;
 
-        let (_, finder) = app.add_window(|ctx| FileFinder::new(settings, workspace.clone(), ctx));
+        let (_, finder) =
+            app.add_window(|ctx| FileFinder::new(app_state.settings, workspace.clone(), ctx));
 
         // Run a search that matches two files with the same relative path.
         finder

zed/src/language.rs šŸ”—

@@ -0,0 +1,152 @@
+use crate::settings::{Theme, ThemeMap};
+use parking_lot::Mutex;
+use rust_embed::RustEmbed;
+use serde::Deserialize;
+use std::{path::Path, str, sync::Arc};
+use tree_sitter::{Language as Grammar, Query};
+pub use tree_sitter::{Parser, Tree};
+
+#[derive(RustEmbed)]
+#[folder = "languages"]
+pub struct LanguageDir;
+
+#[derive(Default, Deserialize)]
+pub struct LanguageConfig {
+    pub name: String,
+    pub path_suffixes: Vec<String>,
+}
+
+#[derive(Deserialize)]
+pub struct BracketPair {
+    pub start: String,
+    pub end: String,
+}
+
+pub struct Language {
+    pub config: LanguageConfig,
+    pub grammar: Grammar,
+    pub highlight_query: Query,
+    pub brackets_query: Query,
+    pub theme_mapping: Mutex<ThemeMap>,
+}
+
+pub struct LanguageRegistry {
+    languages: Vec<Arc<Language>>,
+}
+
+impl Language {
+    pub fn theme_mapping(&self) -> ThemeMap {
+        self.theme_mapping.lock().clone()
+    }
+
+    pub fn set_theme(&self, theme: &Theme) {
+        *self.theme_mapping.lock() = ThemeMap::new(self.highlight_query.capture_names(), theme);
+    }
+}
+
+impl LanguageRegistry {
+    pub fn new() -> Self {
+        let grammar = tree_sitter_rust::language();
+        let rust_config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap()).unwrap();
+        let rust_language = Language {
+            config: rust_config,
+            grammar,
+            highlight_query: Self::load_query(grammar, "rust/highlights.scm"),
+            brackets_query: Self::load_query(grammar, "rust/brackets.scm"),
+            theme_mapping: Mutex::new(ThemeMap::default()),
+        };
+
+        Self {
+            languages: vec![Arc::new(rust_language)],
+        }
+    }
+
+    pub fn set_theme(&self, theme: &Theme) {
+        for language in &self.languages {
+            language.set_theme(theme);
+        }
+    }
+
+    pub fn select_language(&self, path: impl AsRef<Path>) -> Option<&Arc<Language>> {
+        let path = path.as_ref();
+        let filename = path.file_name().and_then(|name| name.to_str());
+        let extension = path.extension().and_then(|name| name.to_str());
+        let path_suffixes = [extension, filename];
+        self.languages.iter().find(|language| {
+            language
+                .config
+                .path_suffixes
+                .iter()
+                .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())))
+        })
+    }
+
+    fn load_query(grammar: tree_sitter::Language, path: &str) -> Query {
+        Query::new(
+            grammar,
+            str::from_utf8(LanguageDir::get(path).unwrap().as_ref()).unwrap(),
+        )
+        .unwrap()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_select_language() {
+        let grammar = tree_sitter_rust::language();
+        let registry = LanguageRegistry {
+            languages: vec![
+                Arc::new(Language {
+                    config: LanguageConfig {
+                        name: "Rust".to_string(),
+                        path_suffixes: vec!["rs".to_string()],
+                        ..Default::default()
+                    },
+                    grammar,
+                    highlight_query: Query::new(grammar, "").unwrap(),
+                    brackets_query: Query::new(grammar, "").unwrap(),
+                    theme_mapping: Default::default(),
+                }),
+                Arc::new(Language {
+                    config: LanguageConfig {
+                        name: "Make".to_string(),
+                        path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
+                        ..Default::default()
+                    },
+                    grammar,
+                    highlight_query: Query::new(grammar, "").unwrap(),
+                    brackets_query: Query::new(grammar, "").unwrap(),
+                    theme_mapping: Default::default(),
+                }),
+            ],
+        };
+
+        // matching file extension
+        assert_eq!(
+            registry.select_language("zed/lib.rs").map(get_name),
+            Some("Rust")
+        );
+        assert_eq!(
+            registry.select_language("zed/lib.mk").map(get_name),
+            Some("Make")
+        );
+
+        // matching filename
+        assert_eq!(
+            registry.select_language("zed/Makefile").map(get_name),
+            Some("Make")
+        );
+
+        // matching suffix that is not the full file extension or filename
+        assert_eq!(registry.select_language("zed/cars").map(get_name), None);
+        assert_eq!(registry.select_language("zed/a.cars").map(get_name), None);
+        assert_eq!(registry.select_language("zed/sumk").map(get_name), None);
+
+        fn get_name(language: &Arc<Language>) -> &str {
+            language.config.name.as_str()
+        }
+    }
+}

zed/src/lib.rs šŸ”—

@@ -1,6 +1,7 @@
 pub mod assets;
 pub mod editor;
 pub mod file_finder;
+pub mod language;
 pub mod menus;
 mod operation_queue;
 pub mod settings;
@@ -11,3 +12,9 @@ mod time;
 mod util;
 pub mod workspace;
 mod worktree;
+
+#[derive(Clone)]
+pub struct AppState {
+    pub settings: postage::watch::Receiver<settings::Settings>,
+    pub language_registry: std::sync::Arc<language::LanguageRegistry>,
+}

zed/src/main.rs šŸ”—

@@ -4,19 +4,28 @@
 use fs::OpenOptions;
 use log::LevelFilter;
 use simplelog::SimpleLogger;
-use std::{fs, path::PathBuf};
+use std::{fs, path::PathBuf, sync::Arc};
 use zed::{
-    assets, editor, file_finder, menus, settings,
+    assets, editor, file_finder, language, menus, settings,
     workspace::{self, OpenParams},
+    AppState,
 };
 
 fn main() {
     init_logger();
 
     let app = gpui::App::new(assets::Assets).unwrap();
-    let (_, settings_rx) = settings::channel(&app.font_cache()).unwrap();
+
+    let (_, settings) = settings::channel(&app.font_cache()).unwrap();
+    let language_registry = Arc::new(language::LanguageRegistry::new());
+    language_registry.set_theme(&settings.borrow().theme);
+    let app_state = AppState {
+        language_registry,
+        settings,
+    };
+
     app.run(move |ctx| {
-        ctx.set_menus(menus::menus(settings_rx.clone()));
+        ctx.set_menus(menus::menus(app_state.settings.clone()));
         workspace::init(ctx);
         editor::init(ctx);
         file_finder::init(ctx);
@@ -31,7 +40,7 @@ fn main() {
                 "workspace:open_paths",
                 OpenParams {
                     paths,
-                    settings: settings_rx,
+                    app_state: app_state.clone(),
                 },
             );
         }

zed/src/settings.rs šŸ”—

@@ -1,6 +1,15 @@
-use anyhow::Result;
-use gpui::font_cache::{FamilyId, FontCache};
+use super::assets::Assets;
+use anyhow::{anyhow, Context, Result};
+use gpui::{
+    color::ColorU,
+    font_cache::{FamilyId, FontCache},
+    fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight},
+};
 use postage::watch;
+use serde::Deserialize;
+use std::{collections::HashMap, sync::Arc};
+
+const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX);
 
 #[derive(Clone)]
 pub struct Settings {
@@ -9,8 +18,23 @@ pub struct Settings {
     pub tab_size: usize,
     pub ui_font_family: FamilyId,
     pub ui_font_size: f32,
+    pub theme: Arc<Theme>,
+}
+
+#[derive(Clone, Default)]
+pub struct Theme {
+    pub background_color: ColorU,
+    pub line_number_color: ColorU,
+    pub default_text_color: ColorU,
+    syntax_styles: Vec<(String, ColorU, FontProperties)>,
 }
 
+#[derive(Clone, Debug)]
+pub struct ThemeMap(Arc<[StyleId]>);
+
+#[derive(Clone, Copy, Debug)]
+pub struct StyleId(u32);
+
 impl Settings {
     pub fn new(font_cache: &FontCache) -> Result<Self> {
         Ok(Self {
@@ -19,12 +43,259 @@ impl Settings {
             tab_size: 4,
             ui_font_family: font_cache.load_family(&["SF Pro", "Helvetica"])?,
             ui_font_size: 12.0,
+            theme: Arc::new(
+                Theme::parse(Assets::get("themes/light.toml").unwrap())
+                    .expect("Failed to parse built-in theme"),
+            ),
         })
     }
 }
 
+impl Theme {
+    pub fn parse(source: impl AsRef<[u8]>) -> Result<Self> {
+        #[derive(Deserialize)]
+        struct ThemeToml {
+            #[serde(default)]
+            syntax: HashMap<String, StyleToml>,
+            #[serde(default)]
+            ui: HashMap<String, u32>,
+        }
+
+        #[derive(Deserialize)]
+        #[serde(untagged)]
+        enum StyleToml {
+            Color(u32),
+            Full {
+                color: Option<u32>,
+                weight: Option<toml::Value>,
+                #[serde(default)]
+                italic: bool,
+            },
+        }
+
+        let theme_toml: ThemeToml =
+            toml::from_slice(source.as_ref()).context("failed to parse theme TOML")?;
+
+        let mut syntax_styles = Vec::<(String, ColorU, FontProperties)>::new();
+        for (key, style) in theme_toml.syntax {
+            let (color, weight, italic) = match style {
+                StyleToml::Color(color) => (color, None, false),
+                StyleToml::Full {
+                    color,
+                    weight,
+                    italic,
+                } => (color.unwrap_or(0), weight, italic),
+            };
+            match syntax_styles.binary_search_by_key(&&key, |e| &e.0) {
+                Ok(i) | Err(i) => {
+                    let mut properties = FontProperties::new();
+                    properties.weight = deserialize_weight(weight)?;
+                    if italic {
+                        properties.style = FontStyle::Italic;
+                    }
+                    syntax_styles.insert(i, (key, deserialize_color(color), properties));
+                }
+            }
+        }
+
+        let background_color = theme_toml
+            .ui
+            .get("background")
+            .copied()
+            .map_or(ColorU::from_u32(0xffffffff), deserialize_color);
+        let line_number_color = theme_toml
+            .ui
+            .get("line_numbers")
+            .copied()
+            .map_or(ColorU::black(), deserialize_color);
+        let default_text_color = theme_toml
+            .ui
+            .get("text")
+            .copied()
+            .map_or(ColorU::black(), deserialize_color);
+
+        Ok(Theme {
+            background_color,
+            line_number_color,
+            default_text_color,
+            syntax_styles,
+        })
+    }
+
+    pub fn syntax_style(&self, id: StyleId) -> (ColorU, FontProperties) {
+        self.syntax_styles
+            .get(id.0 as usize)
+            .map_or((self.default_text_color, FontProperties::new()), |entry| {
+                (entry.1, entry.2)
+            })
+    }
+
+    #[cfg(test)]
+    pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> {
+        self.syntax_styles.get(id.0 as usize).map(|e| e.0.as_str())
+    }
+}
+
+impl ThemeMap {
+    pub fn new(capture_names: &[String], theme: &Theme) -> Self {
+        // For each capture name in the highlight query, find the longest
+        // key in the theme's syntax styles that matches all of the
+        // dot-separated components of the capture name.
+        ThemeMap(
+            capture_names
+                .iter()
+                .map(|capture_name| {
+                    theme
+                        .syntax_styles
+                        .iter()
+                        .enumerate()
+                        .filter_map(|(i, (key, _, _))| {
+                            let mut len = 0;
+                            let capture_parts = capture_name.split('.');
+                            for key_part in key.split('.') {
+                                if capture_parts.clone().any(|part| part == key_part) {
+                                    len += 1;
+                                } else {
+                                    return None;
+                                }
+                            }
+                            Some((i, len))
+                        })
+                        .max_by_key(|(_, len)| *len)
+                        .map_or(DEFAULT_STYLE_ID, |(i, _)| StyleId(i as u32))
+                })
+                .collect(),
+        )
+    }
+
+    pub fn get(&self, capture_id: u32) -> StyleId {
+        self.0
+            .get(capture_id as usize)
+            .copied()
+            .unwrap_or(DEFAULT_STYLE_ID)
+    }
+}
+
+impl Default for ThemeMap {
+    fn default() -> Self {
+        Self(Arc::new([]))
+    }
+}
+
+impl Default for StyleId {
+    fn default() -> Self {
+        DEFAULT_STYLE_ID
+    }
+}
+
 pub fn channel(
     font_cache: &FontCache,
 ) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
     Ok(watch::channel_with(Settings::new(font_cache)?))
 }
+
+fn deserialize_color(color: u32) -> ColorU {
+    ColorU::from_u32((color << 8) + 0xFF)
+}
+
+fn deserialize_weight(weight: Option<toml::Value>) -> Result<FontWeight> {
+    match &weight {
+        None => return Ok(FontWeight::NORMAL),
+        Some(toml::Value::Integer(i)) => return Ok(FontWeight(*i as f32)),
+        Some(toml::Value::String(s)) => match s.as_str() {
+            "normal" => return Ok(FontWeight::NORMAL),
+            "bold" => return Ok(FontWeight::BOLD),
+            "light" => return Ok(FontWeight::LIGHT),
+            "semibold" => return Ok(FontWeight::SEMIBOLD),
+            _ => {}
+        },
+        _ => {}
+    }
+    Err(anyhow!("Invalid weight {}", weight.unwrap()))
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_parse_theme() {
+        let theme = Theme::parse(
+            r#"
+            [ui]
+            background = 0x00ed00
+            line_numbers = 0xdddddd
+
+            [syntax]
+            "beta.two" = 0xAABBCC
+            "alpha.one" = {color = 0x112233, weight = "bold"}
+            "gamma.three" = {weight = "light", italic = true}
+            "#,
+        )
+        .unwrap();
+
+        assert_eq!(theme.background_color, ColorU::from_u32(0x00ED00FF));
+        assert_eq!(theme.line_number_color, ColorU::from_u32(0xddddddff));
+        assert_eq!(
+            theme.syntax_styles,
+            &[
+                (
+                    "alpha.one".to_string(),
+                    ColorU::from_u32(0x112233FF),
+                    *FontProperties::new().weight(FontWeight::BOLD)
+                ),
+                (
+                    "beta.two".to_string(),
+                    ColorU::from_u32(0xAABBCCFF),
+                    *FontProperties::new().weight(FontWeight::NORMAL)
+                ),
+                (
+                    "gamma.three".to_string(),
+                    ColorU::from_u32(0x000000FF),
+                    *FontProperties::new()
+                        .weight(FontWeight::LIGHT)
+                        .style(FontStyle::Italic),
+                ),
+            ]
+        );
+    }
+
+    #[test]
+    fn test_parse_empty_theme() {
+        Theme::parse("").unwrap();
+    }
+
+    #[test]
+    fn test_theme_map() {
+        let theme = Theme {
+            default_text_color: Default::default(),
+            background_color: ColorU::default(),
+            line_number_color: ColorU::default(),
+            syntax_styles: [
+                ("function", ColorU::from_u32(0x100000ff)),
+                ("function.method", ColorU::from_u32(0x200000ff)),
+                ("function.async", ColorU::from_u32(0x300000ff)),
+                ("variable.builtin.self.rust", ColorU::from_u32(0x400000ff)),
+                ("variable.builtin", ColorU::from_u32(0x500000ff)),
+                ("variable", ColorU::from_u32(0x600000ff)),
+            ]
+            .iter()
+            .map(|e| (e.0.to_string(), e.1, FontProperties::new()))
+            .collect(),
+        };
+
+        let capture_names = &[
+            "function.special".to_string(),
+            "function.async.rust".to_string(),
+            "variable.builtin.self".to_string(),
+        ];
+
+        let map = ThemeMap::new(capture_names, &theme);
+        assert_eq!(theme.syntax_style_name(map.get(0)), Some("function"));
+        assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async"));
+        assert_eq!(
+            theme.syntax_style_name(map.get(2)),
+            Some("variable.builtin")
+        );
+    }
+}

zed/src/test.rs šŸ”—

@@ -1,9 +1,11 @@
-use crate::time::ReplicaId;
+use crate::{language::LanguageRegistry, settings, time::ReplicaId, AppState};
 use ctor::ctor;
+use gpui::AppContext;
 use rand::Rng;
 use std::{
     collections::BTreeMap,
     path::{Path, PathBuf},
+    sync::Arc,
 };
 use tempdir::TempDir;
 
@@ -141,3 +143,12 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
         panic!("You must pass a JSON object to this helper")
     }
 }
+
+pub fn build_app_state(ctx: &AppContext) -> AppState {
+    let settings = settings::channel(&ctx.font_cache()).unwrap().1;
+    let language_registry = Arc::new(LanguageRegistry::new());
+    AppState {
+        settings,
+        language_registry,
+    }
+}

zed/src/workspace.rs šŸ”—

@@ -2,9 +2,11 @@ pub mod pane;
 pub mod pane_group;
 use crate::{
     editor::{Buffer, BufferView},
+    language::LanguageRegistry,
     settings::Settings,
     time::ReplicaId,
     worktree::{FileHandle, Worktree, WorktreeHandle},
+    AppState,
 };
 use futures_core::Future;
 use gpui::{
@@ -40,11 +42,11 @@ pub fn init(app: &mut MutableAppContext) {
 
 pub struct OpenParams {
     pub paths: Vec<PathBuf>,
-    pub settings: watch::Receiver<Settings>,
+    pub app_state: AppState,
 }
 
-fn open(settings: &watch::Receiver<Settings>, ctx: &mut MutableAppContext) {
-    let settings = settings.clone();
+fn open(app_state: &AppState, ctx: &mut MutableAppContext) {
+    let app_state = app_state.clone();
     ctx.prompt_for_paths(
         PathPromptOptions {
             files: true,
@@ -53,7 +55,7 @@ fn open(settings: &watch::Receiver<Settings>, ctx: &mut MutableAppContext) {
         },
         move |paths, ctx| {
             if let Some(paths) = paths {
-                ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, settings });
+                ctx.dispatch_global_action("workspace:open_paths", OpenParams { paths, app_state });
             }
         },
     );
@@ -84,7 +86,12 @@ fn open_paths(params: &OpenParams, app: &mut MutableAppContext) {
 
     // Add a new workspace if necessary
     app.add_window(|ctx| {
-        let mut view = Workspace::new(0, params.settings.clone(), ctx);
+        let mut view = Workspace::new(
+            0,
+            params.app_state.settings.clone(),
+            params.app_state.language_registry.clone(),
+            ctx,
+        );
         let open_paths = view.open_paths(&params.paths, ctx);
         ctx.foreground().spawn(open_paths).detach();
         view
@@ -284,6 +291,7 @@ pub struct State {
 
 pub struct Workspace {
     pub settings: watch::Receiver<Settings>,
+    language_registry: Arc<LanguageRegistry>,
     modal: Option<AnyViewHandle>,
     center: PaneGroup,
     panes: Vec<ViewHandle<Pane>>,
@@ -301,6 +309,7 @@ impl Workspace {
     pub fn new(
         replica_id: ReplicaId,
         settings: watch::Receiver<Settings>,
+        language_registry: Arc<LanguageRegistry>,
         ctx: &mut ViewContext<Self>,
     ) -> Self {
         let pane = ctx.add_view(|_| Pane::new(settings.clone()));
@@ -316,6 +325,7 @@ impl Workspace {
             panes: vec![pane.clone()],
             active_pane: pane.clone(),
             settings,
+            language_registry,
             replica_id,
             worktrees: Default::default(),
             items: Default::default(),
@@ -503,6 +513,7 @@ impl Workspace {
             let (mut tx, rx) = postage::watch::channel();
             entry.insert(rx);
             let replica_id = self.replica_id;
+            let language_registry = self.language_registry.clone();
 
             ctx.as_mut()
                 .spawn(|mut ctx| async move {
@@ -512,7 +523,14 @@ impl Workspace {
 
                     *tx.borrow_mut() = Some(match history {
                         Ok(history) => Ok(Box::new(ctx.add_model(|ctx| {
-                            Buffer::from_history(replica_id, history, Some(file), ctx)
+                            let language = language_registry.select_language(path);
+                            Buffer::from_history(
+                                replica_id,
+                                history,
+                                Some(file),
+                                language.cloned(),
+                                ctx,
+                            )
                         }))),
                         Err(error) => Err(Arc::new(error)),
                     })
@@ -757,14 +775,17 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::{editor::BufferView, settings, test::temp_tree};
+    use crate::{
+        editor::BufferView,
+        test::{build_app_state, temp_tree},
+    };
     use serde_json::json;
     use std::{collections::HashSet, fs};
     use tempdir::TempDir;
 
     #[gpui::test]
     fn test_open_paths_action(app: &mut gpui::MutableAppContext) {
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = build_app_state(app.as_ref());
 
         init(app);
 
@@ -790,7 +811,7 @@ mod tests {
                     dir.path().join("a").to_path_buf(),
                     dir.path().join("b").to_path_buf(),
                 ],
-                settings: settings.clone(),
+                app_state: app_state.clone(),
             },
         );
         assert_eq!(app.window_ids().count(), 1);
@@ -799,7 +820,7 @@ mod tests {
             "workspace:open_paths",
             OpenParams {
                 paths: vec![dir.path().join("a").to_path_buf()],
-                settings: settings.clone(),
+                app_state: app_state.clone(),
             },
         );
         assert_eq!(app.window_ids().count(), 1);
@@ -815,7 +836,7 @@ mod tests {
                     dir.path().join("b").to_path_buf(),
                     dir.path().join("c").to_path_buf(),
                 ],
-                settings: settings.clone(),
+                app_state: app_state.clone(),
             },
         );
         assert_eq!(app.window_ids().count(), 2);
@@ -831,10 +852,11 @@ mod tests {
             },
         }));
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
 
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir.path(), ctx);
             workspace
         });
@@ -935,9 +957,10 @@ mod tests {
             "b.txt": "",
         }));
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir1.path(), ctx);
             workspace
         });
@@ -1003,9 +1026,10 @@ mod tests {
             "a.txt": "",
         }));
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (window_id, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir.path(), ctx);
             workspace
         });
@@ -1046,9 +1070,10 @@ mod tests {
     #[gpui::test]
     async fn test_open_and_save_new_file(mut app: gpui::TestAppContext) {
         let dir = TempDir::new("test-new-file").unwrap();
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (_, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir.path(), ctx);
             workspace
         });
@@ -1150,9 +1175,10 @@ mod tests {
             },
         }));
 
-        let settings = settings::channel(&app.font_cache()).unwrap().1;
+        let app_state = app.read(build_app_state);
         let (window_id, workspace) = app.add_window(|ctx| {
-            let mut workspace = Workspace::new(0, settings, ctx);
+            let mut workspace =
+                Workspace::new(0, app_state.settings, app_state.language_registry, ctx);
             workspace.add_worktree(dir.path(), ctx);
             workspace
         });

zed/src/worktree.rs šŸ”—

@@ -1483,7 +1483,8 @@ mod tests {
         let path = tree.update(&mut app, |tree, ctx| {
             let path = tree.files(0).next().unwrap().path().clone();
             assert_eq!(path.file_name().unwrap(), "file1");
-            smol::block_on(tree.save(&path, buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap();
+            smol::block_on(tree.save(&path, buffer.read(ctx).snapshot().text(), ctx.as_ref()))
+                .unwrap();
             path
         });
 
@@ -1512,7 +1513,7 @@ mod tests {
         let file = app.update(|ctx| tree.file("", ctx)).await;
         app.update(|ctx| {
             assert_eq!(file.path().file_name(), None);
-            smol::block_on(file.save(buffer.read(ctx).snapshot(), ctx.as_ref())).unwrap();
+            smol::block_on(file.save(buffer.read(ctx).snapshot().text(), ctx.as_ref())).unwrap();
         });
 
         let history = app.read(|ctx| file.load_history(ctx)).await.unwrap();