Merge pull request #1404 from zed-industries/html-support

Max Brunsfeld created

Basic html support

Change summary

Cargo.lock                                   |  35 
crates/editor/Cargo.toml                     |   4 
crates/editor/src/editor.rs                  | 862 ++++++++++++---------
crates/editor/src/multi_buffer.rs            |  46 
crates/language/Cargo.toml                   |   2 
crates/language/src/buffer.rs                |  74 +
crates/language/src/language.rs              |  61 +
crates/language/src/syntax_map.rs            | 111 +
crates/language/src/tests.rs                 | 118 ++
crates/zed/Cargo.toml                        |   2 
crates/zed/src/languages.rs                  |  17 
crates/zed/src/languages/c.rs                |   7 
crates/zed/src/languages/css/brackets.scm    |   3 
crates/zed/src/languages/css/config.toml     |   9 
crates/zed/src/languages/css/highlights.scm  |  76 +
crates/zed/src/languages/css/indents.scm     |   1 
crates/zed/src/languages/elixir.rs           |   4 
crates/zed/src/languages/go.rs               |   4 
crates/zed/src/languages/html.rs             | 101 ++
crates/zed/src/languages/html/brackets.scm   |   2 
crates/zed/src/languages/html/config.toml    |  12 
crates/zed/src/languages/html/highlights.scm |  15 
crates/zed/src/languages/html/indents.scm    |   6 
crates/zed/src/languages/html/injections.scm |   7 
crates/zed/src/languages/html/outline.scm    |   0 
crates/zed/src/languages/python.rs           |   7 
crates/zed/src/languages/rust.rs             |   6 
crates/zed/src/languages/typescript.rs       |   8 
crates/zed/src/zed.rs                        |   4 
29 files changed, 1,118 insertions(+), 486 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1722,6 +1722,8 @@ dependencies = [
  "text",
  "theme",
  "tree-sitter",
+ "tree-sitter-html",
+ "tree-sitter-javascript",
  "tree-sitter-rust",
  "unindent",
  "util",
@@ -2896,6 +2898,8 @@ dependencies = [
  "text",
  "theme",
  "tree-sitter",
+ "tree-sitter-html",
+ "tree-sitter-javascript",
  "tree-sitter-json 0.19.0",
  "tree-sitter-python",
  "tree-sitter-rust",
@@ -6075,6 +6079,15 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-css"
+version = "0.19.0"
+source = "git+https://github.com/tree-sitter/tree-sitter-css?rev=769203d0f9abe1a9a691ac2b9fe4bb4397a73c51#769203d0f9abe1a9a691ac2b9fe4bb4397a73c51"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-elixir"
 version = "0.19.0"
@@ -6093,6 +6106,26 @@ dependencies = [
  "tree-sitter",
 ]
 
+[[package]]
+name = "tree-sitter-html"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "184e6b77953a354303dc87bf5fe36558c83569ce92606e7b382a0dc1b7443443"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
+[[package]]
+name = "tree-sitter-javascript"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2490fab08630b2c8943c320f7b63473cbf65511c8d83aec551beb9b4375906ed"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
 [[package]]
 name = "tree-sitter-json"
 version = "0.19.0"
@@ -7279,8 +7312,10 @@ dependencies = [
  "tree-sitter",
  "tree-sitter-c",
  "tree-sitter-cpp",
+ "tree-sitter-css",
  "tree-sitter-elixir",
  "tree-sitter-go",
+ "tree-sitter-html",
  "tree-sitter-json 0.20.0",
  "tree-sitter-markdown",
  "tree-sitter-python",

crates/editor/Cargo.toml 🔗

@@ -52,6 +52,8 @@ serde = { version = "1.0", features = ["derive", "rc"] }
 smallvec = { version = "1.6", features = ["union"] }
 smol = "1.2"
 tree-sitter-rust = { version = "*", optional = true }
+tree-sitter-html = { version = "*", optional = true }
+tree-sitter-javascript = { version = "*", optional = true }
 
 [dev-dependencies]
 text = { path = "../text", features = ["test-support"] }
@@ -68,3 +70,5 @@ rand = "0.8"
 unindent = "0.1.7"
 tree-sitter = "0.20"
 tree-sitter-rust = "0.20"
+tree-sitter-html = "0.19"
+tree-sitter-javascript = "0.20"

crates/editor/src/editor.rs 🔗

@@ -410,7 +410,7 @@ pub struct Editor {
     add_selections_state: Option<AddSelectionsState>,
     select_next_state: Option<SelectNextState>,
     selection_history: SelectionHistory,
-    autoclose_stack: InvalidationStack<BracketPairState>,
+    autoclose_regions: Vec<AutocloseRegion>,
     snippet_stack: InvalidationStack<SnippetState>,
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
     ime_transaction: Option<TransactionId>,
@@ -569,8 +569,10 @@ struct SelectNextState {
     done: bool,
 }
 
-struct BracketPairState {
-    ranges: Vec<Range<Anchor>>,
+#[derive(Debug)]
+struct AutocloseRegion {
+    selection_id: usize,
+    range: Range<Anchor>,
     pair: BracketPair,
 }
 
@@ -1010,7 +1012,7 @@ impl Editor {
             add_selections_state: None,
             select_next_state: None,
             selection_history: Default::default(),
-            autoclose_stack: Default::default(),
+            autoclose_regions: Default::default(),
             snippet_stack: Default::default(),
             select_larger_syntax_node_stack: Vec::new(),
             ime_transaction: Default::default(),
@@ -1116,7 +1118,7 @@ impl Editor {
         &self,
         point: T,
         cx: &'a AppContext,
-    ) -> Option<&'a Arc<Language>> {
+    ) -> Option<Arc<Language>> {
         self.buffer.read(cx).language_at(point, cx)
     }
 
@@ -1401,8 +1403,7 @@ impl Editor {
         self.add_selections_state = None;
         self.select_next_state = None;
         self.select_larger_syntax_node_stack.clear();
-        self.autoclose_stack
-            .invalidate(&self.selections.disjoint_anchors(), buffer);
+        self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
         self.snippet_stack
             .invalidate(&self.selections.disjoint_anchors(), buffer);
         self.take_rename(false, cx);
@@ -1849,15 +1850,159 @@ impl Editor {
             return;
         }
 
-        if !self.skip_autoclose_end(text, cx) {
-            self.transact(cx, |this, cx| {
-                if !this.surround_with_bracket_pair(text, cx) {
-                    this.insert(text, cx);
-                    this.autoclose_bracket_pairs(cx);
+        let text: Arc<str> = text.into();
+        let selections = self.selections.all_adjusted(cx);
+        let mut edits = Vec::new();
+        let mut new_selections = Vec::with_capacity(selections.len());
+        let mut new_autoclose_regions = Vec::new();
+        let snapshot = self.buffer.read(cx).read(cx);
+
+        for (selection, autoclose_region) in
+            self.selections_with_autoclose_regions(selections, &snapshot)
+        {
+            if let Some(language) = snapshot.language_at(selection.head()) {
+                // Determine if the inserted text matches the opening or closing
+                // bracket of any of this language's bracket pairs.
+                let mut bracket_pair = None;
+                let mut is_bracket_pair_start = false;
+                for pair in language.brackets() {
+                    if pair.start.ends_with(text.as_ref()) {
+                        bracket_pair = Some(pair.clone());
+                        is_bracket_pair_start = true;
+                        break;
+                    } else if pair.end.as_str() == text.as_ref() {
+                        bracket_pair = Some(pair.clone());
+                        break;
+                    }
                 }
-            });
-            self.trigger_completion_on_input(text, cx);
+
+                if let Some(bracket_pair) = bracket_pair {
+                    if selection.is_empty() {
+                        if is_bracket_pair_start {
+                            let prefix_len = bracket_pair.start.len() - text.len();
+
+                            // If the inserted text is a suffix of an opening bracket and the
+                            // selection is preceded by the rest of the opening bracket, then
+                            // insert the closing bracket.
+                            let following_text_allows_autoclose = snapshot
+                                .chars_at(selection.start)
+                                .next()
+                                .map_or(true, |c| language.should_autoclose_before(c));
+                            let preceding_text_matches_prefix = prefix_len == 0
+                                || (selection.start.column >= (prefix_len as u32)
+                                    && snapshot.contains_str_at(
+                                        Point::new(
+                                            selection.start.row,
+                                            selection.start.column - (prefix_len as u32),
+                                        ),
+                                        &bracket_pair.start[..prefix_len],
+                                    ));
+                            if following_text_allows_autoclose && preceding_text_matches_prefix {
+                                let anchor = snapshot.anchor_before(selection.end);
+                                new_selections
+                                    .push((selection.map(|_| anchor.clone()), text.len()));
+                                new_autoclose_regions.push((
+                                    anchor.clone(),
+                                    text.len(),
+                                    selection.id,
+                                    bracket_pair.clone(),
+                                ));
+                                edits.push((
+                                    selection.range(),
+                                    format!("{}{}", text, bracket_pair.end).into(),
+                                ));
+                                continue;
+                            }
+                        } else if let Some(region) = autoclose_region {
+                            // If the selection is followed by an auto-inserted closing bracket,
+                            // then don't insert anything else; just move the selection past the
+                            // closing bracket.
+                            let should_skip = selection.end == region.range.end.to_point(&snapshot);
+                            if should_skip {
+                                let anchor = snapshot.anchor_after(selection.end);
+                                new_selections.push((
+                                    selection.map(|_| anchor.clone()),
+                                    region.pair.end.len(),
+                                ));
+                                continue;
+                            }
+                        }
+                    }
+                    // If an opening bracket is typed while text is selected, then
+                    // surround that text with the bracket pair.
+                    else if is_bracket_pair_start {
+                        edits.push((selection.start..selection.start, text.clone()));
+                        edits.push((
+                            selection.end..selection.end,
+                            bracket_pair.end.as_str().into(),
+                        ));
+                        new_selections.push((
+                            Selection {
+                                id: selection.id,
+                                start: snapshot.anchor_after(selection.start),
+                                end: snapshot.anchor_before(selection.end),
+                                reversed: selection.reversed,
+                                goal: selection.goal,
+                            },
+                            0,
+                        ));
+                        continue;
+                    }
+                }
+            }
+
+            // If not handling any auto-close operation, then just replace the selected
+            // text with the given input and move the selection to the end of the
+            // newly inserted text.
+            let anchor = snapshot.anchor_after(selection.end);
+            new_selections.push((selection.map(|_| anchor.clone()), 0));
+            edits.push((selection.start..selection.end, text.clone()));
         }
+
+        drop(snapshot);
+        self.transact(cx, |this, cx| {
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, Some(AutoindentMode::EachLine), cx);
+            });
+
+            let new_anchor_selections = new_selections.iter().map(|e| &e.0);
+            let new_selection_deltas = new_selections.iter().map(|e| e.1);
+            let snapshot = this.buffer.read(cx).read(cx);
+            let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &snapshot)
+                .zip(new_selection_deltas)
+                .map(|(selection, delta)| selection.map(|e| e + delta))
+                .collect::<Vec<_>>();
+
+            let mut i = 0;
+            for (position, delta, selection_id, pair) in new_autoclose_regions {
+                let position = position.to_offset(&snapshot) + delta;
+                let start = snapshot.anchor_before(position);
+                let end = snapshot.anchor_after(position);
+                while let Some(existing_state) = this.autoclose_regions.get(i) {
+                    match existing_state.range.start.cmp(&start, &snapshot) {
+                        Ordering::Less => i += 1,
+                        Ordering::Greater => break,
+                        Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) {
+                            Ordering::Less => i += 1,
+                            Ordering::Equal => break,
+                            Ordering::Greater => break,
+                        },
+                    }
+                }
+                this.autoclose_regions.insert(
+                    i,
+                    AutocloseRegion {
+                        selection_id,
+                        range: start..end,
+                        pair,
+                    },
+                );
+            }
+
+            drop(snapshot);
+            this.change_selections(None, cx, |s| s.select(new_selections));
+            this.trigger_completion_on_input(&text, cx);
+        });
     }
 
     pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext<Self>) {
@@ -1876,7 +2021,7 @@ impl Editor {
                         let end = selection.end;
 
                         let mut insert_extra_newline = false;
-                        if let Some(language) = buffer.language() {
+                        if let Some(language) = buffer.language_at(start) {
                             let leading_whitespace_len = buffer
                                 .reversed_chars_at(start)
                                 .take_while(|c| c.is_whitespace() && *c != '\n')
@@ -2029,232 +2174,89 @@ impl Editor {
         }
     }
 
-    fn surround_with_bracket_pair(&mut self, text: &str, cx: &mut ViewContext<Self>) -> bool {
-        let snapshot = self.buffer.read(cx).snapshot(cx);
-        if let Some(pair) = snapshot
-            .language()
-            .and_then(|language| language.brackets().iter().find(|b| b.start == text))
-            .cloned()
-        {
-            if self
-                .selections
-                .all::<usize>(cx)
-                .iter()
-                .any(|selection| selection.is_empty())
-            {
-                return false;
-            }
-
-            let mut selections = self.selections.disjoint_anchors().to_vec();
-            for selection in &mut selections {
-                selection.end = selection.end.bias_left(&snapshot);
-            }
-            drop(snapshot);
-
-            self.buffer.update(cx, |buffer, cx| {
-                let pair_start: Arc<str> = pair.start.clone().into();
-                let pair_end: Arc<str> = pair.end.clone().into();
-                buffer.edit(
-                    selections.iter().flat_map(|s| {
-                        [
-                            (s.start.clone()..s.start.clone(), pair_start.clone()),
-                            (s.end.clone()..s.end.clone(), pair_end.clone()),
-                        ]
-                    }),
-                    None,
-                    cx,
-                );
-            });
-
-            let snapshot = self.buffer.read(cx).read(cx);
-            for selection in &mut selections {
-                selection.end = selection.end.bias_right(&snapshot);
-            }
-            drop(snapshot);
-
-            self.change_selections(None, cx, |s| s.select_anchors(selections));
-            true
-        } else {
-            false
-        }
-    }
-
-    fn autoclose_bracket_pairs(&mut self, cx: &mut ViewContext<Self>) {
+    /// If any empty selections is touching the start of its innermost containing autoclose
+    /// region, expand it to select the brackets.
+    fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
         let selections = self.selections.all::<usize>(cx);
-        let mut bracket_pair_state = None;
-        let mut new_selections = None;
-        self.buffer.update(cx, |buffer, cx| {
-            let mut snapshot = buffer.snapshot(cx);
-            let left_biased_selections = selections
-                .iter()
-                .map(|selection| selection.map(|p| snapshot.anchor_before(p)))
-                .collect::<Vec<_>>();
-
-            let autoclose_pair = snapshot.language().and_then(|language| {
-                let first_selection_start = selections.first().unwrap().start;
-                let pair = language.brackets().iter().find(|pair| {
-                    pair.close
-                        && snapshot.contains_str_at(
-                            first_selection_start.saturating_sub(pair.start.len()),
-                            &pair.start,
-                        )
-                });
-                pair.and_then(|pair| {
-                    let should_autoclose = selections.iter().all(|selection| {
-                        // Ensure all selections are parked at the end of a pair start.
-                        if snapshot.contains_str_at(
-                            selection.start.saturating_sub(pair.start.len()),
-                            &pair.start,
-                        ) {
-                            snapshot
-                                .chars_at(selection.start)
-                                .next()
-                                .map_or(true, |c| language.should_autoclose_before(c))
-                        } else {
-                            false
+        let buffer = self.buffer.read(cx).read(cx);
+        let mut new_selections = Vec::new();
+        for (mut selection, region) in self.selections_with_autoclose_regions(selections, &buffer) {
+            if let (Some(region), true) = (region, selection.is_empty()) {
+                let mut range = region.range.to_offset(&buffer);
+                if selection.start == range.start {
+                    if range.start >= region.pair.start.len() {
+                        range.start -= region.pair.start.len();
+                        if buffer.contains_str_at(range.start, &region.pair.start) {
+                            if buffer.contains_str_at(range.end, &region.pair.end) {
+                                range.end += region.pair.end.len();
+                                selection.start = range.start;
+                                selection.end = range.end;
+                            }
                         }
-                    });
-
-                    if should_autoclose {
-                        Some(pair.clone())
-                    } else {
-                        None
                     }
-                })
-            });
-
-            if let Some(pair) = autoclose_pair {
-                let selection_ranges = selections
-                    .iter()
-                    .map(|selection| {
-                        let start = selection.start.to_offset(&snapshot);
-                        start..start
-                    })
-                    .collect::<SmallVec<[_; 32]>>();
-
-                let pair_end: Arc<str> = pair.end.clone().into();
-                buffer.edit(
-                    selection_ranges
-                        .iter()
-                        .map(|range| (range.clone(), pair_end.clone())),
-                    None,
-                    cx,
-                );
-                snapshot = buffer.snapshot(cx);
-
-                new_selections = Some(
-                    resolve_multiple::<usize, _>(left_biased_selections.iter(), &snapshot)
-                        .collect::<Vec<_>>(),
-                );
-
-                if pair.end.len() == 1 {
-                    let mut delta = 0;
-                    bracket_pair_state = Some(BracketPairState {
-                        ranges: selections
-                            .iter()
-                            .map(move |selection| {
-                                let offset = selection.start + delta;
-                                delta += 1;
-                                snapshot.anchor_before(offset)..snapshot.anchor_after(offset)
-                            })
-                            .collect(),
-                        pair,
-                    });
                 }
             }
-        });
-
-        if let Some(new_selections) = new_selections {
-            self.change_selections(None, cx, |s| {
-                s.select(new_selections);
-            });
+            new_selections.push(selection);
         }
-        if let Some(bracket_pair_state) = bracket_pair_state {
-            self.autoclose_stack.push(bracket_pair_state);
-        }
-    }
 
-    fn skip_autoclose_end(&mut self, text: &str, cx: &mut ViewContext<Self>) -> bool {
-        let buffer = self.buffer.read(cx).snapshot(cx);
-        let old_selections = self.selections.all::<usize>(cx);
-        let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() {
-            autoclose_pair
-        } else {
-            return false;
-        };
-        if text != autoclose_pair.pair.end {
-            return false;
-        }
-
-        debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len());
-
-        if old_selections
-            .iter()
-            .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer)))
-            .all(|(selection, autoclose_range)| {
-                let autoclose_range_end = autoclose_range.end.to_offset(&buffer);
-                selection.is_empty() && selection.start == autoclose_range_end
-            })
-        {
-            let new_selections = old_selections
-                .into_iter()
-                .map(|selection| {
-                    let cursor = selection.start + 1;
-                    Selection {
-                        id: selection.id,
-                        start: cursor,
-                        end: cursor,
-                        reversed: false,
-                        goal: SelectionGoal::None,
-                    }
-                })
-                .collect();
-            self.autoclose_stack.pop();
-            self.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                s.select(new_selections);
-            });
-            true
-        } else {
-            false
-        }
+        drop(buffer);
+        self.change_selections(None, cx, |selections| selections.select(new_selections));
     }
 
-    fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) -> bool {
-        let buffer = self.buffer.read(cx).snapshot(cx);
-        let old_selections = self.selections.all::<usize>(cx);
-        let autoclose_pair = if let Some(autoclose_pair) = self.autoclose_stack.last() {
-            autoclose_pair
-        } else {
-            return false;
-        };
+    /// Iterate the given selections, and for each one, find the smallest surrounding
+    /// autoclose region. This uses the ordering of the selections and the autoclose
+    /// regions to avoid repeated comparisons.
+    fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>(
+        &'a self,
+        selections: impl IntoIterator<Item = Selection<D>>,
+        buffer: &'a MultiBufferSnapshot,
+    ) -> impl Iterator<Item = (Selection<D>, Option<&'a AutocloseRegion>)> {
+        let mut i = 0;
+        let mut regions = self.autoclose_regions.as_slice();
+        selections.into_iter().map(move |selection| {
+            let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer);
+
+            let mut enclosing = None;
+            while let Some(pair_state) = regions.get(i) {
+                if pair_state.range.end.to_offset(buffer) < range.start {
+                    regions = &regions[i + 1..];
+                    i = 0;
+                } else if pair_state.range.start.to_offset(buffer) > range.end {
+                    break;
+                } else if pair_state.selection_id == selection.id {
+                    enclosing = Some(pair_state);
+                    i += 1;
+                }
+            }
 
-        debug_assert_eq!(old_selections.len(), autoclose_pair.ranges.len());
+            (selection.clone(), enclosing)
+        })
+    }
 
-        let mut new_selections = Vec::new();
-        for (selection, autoclose_range) in old_selections
-            .iter()
-            .zip(autoclose_pair.ranges.iter().map(|r| r.to_offset(&buffer)))
-        {
-            if selection.is_empty()
-                && autoclose_range.is_empty()
-                && selection.start == autoclose_range.start
-            {
-                new_selections.push(Selection {
-                    id: selection.id,
-                    start: selection.start - autoclose_pair.pair.start.len(),
-                    end: selection.end + autoclose_pair.pair.end.len(),
-                    reversed: true,
-                    goal: selection.goal,
-                });
-            } else {
-                return false;
+    /// Remove any autoclose regions that no longer contain their selection.
+    fn invalidate_autoclose_regions(
+        &mut self,
+        mut selections: &[Selection<Anchor>],
+        buffer: &MultiBufferSnapshot,
+    ) {
+        self.autoclose_regions.retain(|state| {
+            let mut i = 0;
+            while let Some(selection) = selections.get(i) {
+                if selection.end.cmp(&state.range.start, buffer).is_lt() {
+                    selections = &selections[1..];
+                    continue;
+                }
+                if selection.start.cmp(&state.range.end, buffer).is_gt() {
+                    break;
+                }
+                if selection.id == state.selection_id {
+                    return true;
+                } else {
+                    i += 1;
+                }
             }
-        }
-
-        self.change_selections(Some(Autoscroll::Fit), cx, |selections| {
-            selections.select(new_selections)
+            false
         });
-        true
     }
 
     fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
@@ -2909,51 +2911,49 @@ impl Editor {
 
     pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
-            if !this.select_autoclose_pair(cx) {
-                let mut selections = this.selections.all::<Point>(cx);
-                if !this.selections.line_mode {
-                    let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
-                    for selection in &mut selections {
-                        if selection.is_empty() {
-                            let old_head = selection.head();
-                            let mut new_head = movement::left(
-                                &display_map,
-                                old_head.to_display_point(&display_map),
-                            )
-                            .to_point(&display_map);
-                            if let Some((buffer, line_buffer_range)) = display_map
-                                .buffer_snapshot
-                                .buffer_line_for_row(old_head.row)
-                            {
-                                let indent_size =
-                                    buffer.indent_size_for_line(line_buffer_range.start.row);
-                                let language_name =
-                                    buffer.language().map(|language| language.name());
-                                let indent_len = match indent_size.kind {
-                                    IndentKind::Space => {
-                                        cx.global::<Settings>().tab_size(language_name.as_deref())
-                                    }
-                                    IndentKind::Tab => NonZeroU32::new(1).unwrap(),
-                                };
-                                if old_head.column <= indent_size.len && old_head.column > 0 {
-                                    let indent_len = indent_len.get();
-                                    new_head = cmp::min(
-                                        new_head,
-                                        Point::new(
-                                            old_head.row,
-                                            ((old_head.column - 1) / indent_len) * indent_len,
-                                        ),
-                                    );
+            this.select_autoclose_pair(cx);
+            let mut selections = this.selections.all::<Point>(cx);
+            if !this.selections.line_mode {
+                let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
+                for selection in &mut selections {
+                    if selection.is_empty() {
+                        let old_head = selection.head();
+                        let mut new_head =
+                            movement::left(&display_map, old_head.to_display_point(&display_map))
+                                .to_point(&display_map);
+                        if let Some((buffer, line_buffer_range)) = display_map
+                            .buffer_snapshot
+                            .buffer_line_for_row(old_head.row)
+                        {
+                            let indent_size =
+                                buffer.indent_size_for_line(line_buffer_range.start.row);
+                            let language_name = buffer
+                                .language_at(line_buffer_range.start)
+                                .map(|language| language.name());
+                            let indent_len = match indent_size.kind {
+                                IndentKind::Space => {
+                                    cx.global::<Settings>().tab_size(language_name.as_deref())
                                 }
+                                IndentKind::Tab => NonZeroU32::new(1).unwrap(),
+                            };
+                            if old_head.column <= indent_size.len && old_head.column > 0 {
+                                let indent_len = indent_len.get();
+                                new_head = cmp::min(
+                                    new_head,
+                                    Point::new(
+                                        old_head.row,
+                                        ((old_head.column - 1) / indent_len) * indent_len,
+                                    ),
+                                );
                             }
-
-                            selection.set_head(new_head, SelectionGoal::None);
                         }
+
+                        selection.set_head(new_head, SelectionGoal::None);
                     }
                 }
-
-                this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
             }
+
+            this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
             this.insert("", cx);
         });
     }
@@ -3957,17 +3957,16 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            if !this.select_autoclose_pair(cx) {
-                this.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                    let line_mode = s.line_mode;
-                    s.move_with(|map, selection| {
-                        if selection.is_empty() && !line_mode {
-                            let cursor = movement::previous_word_start(map, selection.head());
-                            selection.set_head(cursor, SelectionGoal::None);
-                        }
-                    });
+            this.select_autoclose_pair(cx);
+            this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                let line_mode = s.line_mode;
+                s.move_with(|map, selection| {
+                    if selection.is_empty() && !line_mode {
+                        let cursor = movement::previous_word_start(map, selection.head());
+                        selection.set_head(cursor, SelectionGoal::None);
+                    }
                 });
-            }
+            });
             this.insert("", cx);
         });
     }
@@ -3978,17 +3977,16 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) {
         self.transact(cx, |this, cx| {
-            if !this.select_autoclose_pair(cx) {
-                this.change_selections(Some(Autoscroll::Fit), cx, |s| {
-                    let line_mode = s.line_mode;
-                    s.move_with(|map, selection| {
-                        if selection.is_empty() && !line_mode {
-                            let cursor = movement::previous_subword_start(map, selection.head());
-                            selection.set_head(cursor, SelectionGoal::None);
-                        }
-                    });
+            this.select_autoclose_pair(cx);
+            this.change_selections(Some(Autoscroll::Fit), cx, |s| {
+                let line_mode = s.line_mode;
+                s.move_with(|map, selection| {
+                    if selection.is_empty() && !line_mode {
+                        let cursor = movement::previous_subword_start(map, selection.head());
+                        selection.set_head(cursor, SelectionGoal::None);
+                    }
                 });
-            }
+            });
             this.insert("", cx);
         });
     }
@@ -4491,108 +4489,218 @@ impl Editor {
     pub fn toggle_comments(&mut self, _: &ToggleComments, cx: &mut ViewContext<Self>) {
         self.transact(cx, |this, cx| {
             let mut selections = this.selections.all::<Point>(cx);
-            let mut all_selection_lines_are_comments = true;
-            let mut edit_ranges = Vec::new();
+            let mut edits = Vec::new();
+            let mut selection_edit_ranges = Vec::new();
             let mut last_toggled_row = None;
-            this.buffer.update(cx, |buffer, cx| {
-                // TODO: Handle selections that cross excerpts
-                for selection in &mut selections {
-                    // Get the line comment prefix. Split its trailing whitespace into a separate string,
-                    // as that portion won't be used for detecting if a line is a comment.
-                    let full_comment_prefix: Arc<str> = if let Some(prefix) = buffer
-                        .language_at(selection.start, cx)
-                        .and_then(|l| l.line_comment_prefix())
-                    {
-                        prefix.into()
+            let snapshot = this.buffer.read(cx).read(cx);
+            let empty_str: Arc<str> = "".into();
+            let mut suffixes_inserted = Vec::new();
+
+            fn comment_prefix_range(
+                snapshot: &MultiBufferSnapshot,
+                row: u32,
+                comment_prefix: &str,
+                comment_prefix_whitespace: &str,
+            ) -> Range<Point> {
+                let start = Point::new(row, snapshot.indent_size_for_line(row).len);
+
+                let mut line_bytes = snapshot
+                    .bytes_in_range(start..snapshot.max_point())
+                    .flatten()
+                    .copied();
+
+                // If this line currently begins with the line comment prefix, then record
+                // the range containing the prefix.
+                if line_bytes
+                    .by_ref()
+                    .take(comment_prefix.len())
+                    .eq(comment_prefix.bytes())
+                {
+                    // Include any whitespace that matches the comment prefix.
+                    let matching_whitespace_len = line_bytes
+                        .zip(comment_prefix_whitespace.bytes())
+                        .take_while(|(a, b)| a == b)
+                        .count() as u32;
+                    let end = Point::new(
+                        start.row,
+                        start.column + comment_prefix.len() as u32 + matching_whitespace_len,
+                    );
+                    start..end
+                } else {
+                    start..start
+                }
+            }
+
+            fn comment_suffix_range(
+                snapshot: &MultiBufferSnapshot,
+                row: u32,
+                comment_suffix: &str,
+                comment_suffix_has_leading_space: bool,
+            ) -> Range<Point> {
+                let end = Point::new(row, snapshot.line_len(row));
+                let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32);
+
+                let mut line_end_bytes = snapshot
+                    .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end)
+                    .flatten()
+                    .copied();
+
+                let leading_space_len = if suffix_start_column > 0
+                    && line_end_bytes.next() == Some(b' ')
+                    && comment_suffix_has_leading_space
+                {
+                    1
+                } else {
+                    0
+                };
+
+                // If this line currently begins with the line comment prefix, then record
+                // the range containing the prefix.
+                if line_end_bytes.by_ref().eq(comment_suffix.bytes()) {
+                    let start = Point::new(end.row, suffix_start_column - leading_space_len);
+                    start..end
+                } else {
+                    end..end
+                }
+            }
+
+            // TODO: Handle selections that cross excerpts
+            for selection in &mut selections {
+                let language = if let Some(language) = snapshot.language_at(selection.start) {
+                    language
+                } else {
+                    continue;
+                };
+
+                selection_edit_ranges.clear();
+
+                // If multiple selections contain a given row, avoid processing that
+                // row more than once.
+                let mut start_row = selection.start.row;
+                if last_toggled_row == Some(start_row) {
+                    start_row += 1;
+                }
+                let end_row =
+                    if selection.end.row > selection.start.row && selection.end.column == 0 {
+                        selection.end.row - 1
                     } else {
-                        return;
+                        selection.end.row
                     };
-                    let comment_prefix = full_comment_prefix.trim_end_matches(' ');
-                    let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
-                    edit_ranges.clear();
-                    let snapshot = buffer.snapshot(cx);
+                last_toggled_row = Some(end_row);
 
-                    let end_row =
-                        if selection.end.row > selection.start.row && selection.end.column == 0 {
-                            selection.end.row
-                        } else {
-                            selection.end.row + 1
-                        };
+                if start_row > end_row {
+                    continue;
+                }
 
-                    for row in selection.start.row..end_row {
-                        // If multiple selections contain a given row, avoid processing that
-                        // row more than once.
-                        if last_toggled_row == Some(row) {
-                            continue;
-                        } else {
-                            last_toggled_row = Some(row);
-                        }
+                // If the language has line comments, toggle those.
+                if let Some(full_comment_prefix) = language.line_comment_prefix() {
+                    // Split the comment prefix's trailing whitespace into a separate string,
+                    // as that portion won't be used for detecting if a line is a comment.
+                    let comment_prefix = full_comment_prefix.trim_end_matches(' ');
+                    let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
+                    let mut all_selection_lines_are_comments = true;
 
+                    for row in start_row..=end_row {
                         if snapshot.is_line_blank(row) {
                             continue;
                         }
 
-                        let start = Point::new(row, snapshot.indent_size_for_line(row).len);
-                        let mut line_bytes = snapshot
-                            .bytes_in_range(start..snapshot.max_point())
-                            .flatten()
-                            .copied();
-
-                        // If this line currently begins with the line comment prefix, then record
-                        // the range containing the prefix.
-                        if all_selection_lines_are_comments
-                            && line_bytes
-                                .by_ref()
-                                .take(comment_prefix.len())
-                                .eq(comment_prefix.bytes())
-                        {
-                            // Include any whitespace that matches the comment prefix.
-                            let matching_whitespace_len = line_bytes
-                                .zip(comment_prefix_whitespace.bytes())
-                                .take_while(|(a, b)| a == b)
-                                .count()
-                                as u32;
-                            let end = Point::new(
-                                row,
-                                start.column
-                                    + comment_prefix.len() as u32
-                                    + matching_whitespace_len,
-                            );
-                            edit_ranges.push(start..end);
-                        }
-                        // If this line does not begin with the line comment prefix, then record
-                        // the position where the prefix should be inserted.
-                        else {
+                        let prefix_range = comment_prefix_range(
+                            snapshot.deref(),
+                            row,
+                            comment_prefix,
+                            comment_prefix_whitespace,
+                        );
+                        if prefix_range.is_empty() {
                             all_selection_lines_are_comments = false;
-                            edit_ranges.push(start..start);
                         }
+                        selection_edit_ranges.push(prefix_range);
                     }
 
-                    if !edit_ranges.is_empty() {
-                        if all_selection_lines_are_comments {
-                            let empty_str: Arc<str> = "".into();
-                            buffer.edit(
-                                edit_ranges
-                                    .iter()
-                                    .cloned()
-                                    .map(|range| (range, empty_str.clone())),
-                                None,
-                                cx,
-                            );
-                        } else {
-                            let min_column =
-                                edit_ranges.iter().map(|r| r.start.column).min().unwrap();
-                            let edits = edit_ranges.iter().map(|range| {
-                                let position = Point::new(range.start.row, min_column);
-                                (position..position, full_comment_prefix.clone())
-                            });
-                            buffer.edit(edits, None, cx);
-                        }
+                    if all_selection_lines_are_comments {
+                        edits.extend(
+                            selection_edit_ranges
+                                .iter()
+                                .cloned()
+                                .map(|range| (range, empty_str.clone())),
+                        );
+                    } else {
+                        let min_column = selection_edit_ranges
+                            .iter()
+                            .map(|r| r.start.column)
+                            .min()
+                            .unwrap_or(0);
+                        edits.extend(selection_edit_ranges.iter().map(|range| {
+                            let position = Point::new(range.start.row, min_column);
+                            (position..position, full_comment_prefix.clone())
+                        }));
+                    }
+                } else if let Some((full_comment_prefix, comment_suffix)) =
+                    language.block_comment_delimiters()
+                {
+                    let comment_prefix = full_comment_prefix.trim_end_matches(' ');
+                    let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
+                    let prefix_range = comment_prefix_range(
+                        snapshot.deref(),
+                        start_row,
+                        comment_prefix,
+                        comment_prefix_whitespace,
+                    );
+                    let suffix_range = comment_suffix_range(
+                        snapshot.deref(),
+                        end_row,
+                        comment_suffix.trim_start_matches(' '),
+                        comment_suffix.starts_with(' '),
+                    );
+
+                    if prefix_range.is_empty() || suffix_range.is_empty() {
+                        edits.push((
+                            prefix_range.start..prefix_range.start,
+                            full_comment_prefix.clone(),
+                        ));
+                        edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone()));
+                        suffixes_inserted.push((end_row, comment_suffix.len()));
+                    } else {
+                        edits.push((prefix_range, empty_str.clone()));
+                        edits.push((suffix_range, empty_str.clone()));
                     }
+                } else {
+                    continue;
                 }
+            }
+
+            drop(snapshot);
+            this.buffer.update(cx, |buffer, cx| {
+                buffer.edit(edits, None, cx);
             });
 
-            let selections = this.selections.all::<usize>(cx);
+            // Adjust selections so that they end before any comment suffixes that
+            // were inserted.
+            let mut suffixes_inserted = suffixes_inserted.into_iter().peekable();
+            let mut selections = this.selections.all::<Point>(cx);
+            let snapshot = this.buffer.read(cx).read(cx);
+            for selection in &mut selections {
+                while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() {
+                    match row.cmp(&selection.end.row) {
+                        Ordering::Less => {
+                            suffixes_inserted.next();
+                            continue;
+                        }
+                        Ordering::Greater => break,
+                        Ordering::Equal => {
+                            if selection.end.column == snapshot.line_len(row) {
+                                if selection.is_empty() {
+                                    selection.start.column -= suffix_len as u32;
+                                }
+                                selection.end.column -= suffix_len as u32;
+                            }
+                            break;
+                        }
+                    }
+                }
+            }
+
+            drop(snapshot);
             this.change_selections(Some(Autoscroll::Fit), cx, |s| s.select(selections));
         });
     }
@@ -6007,6 +6115,10 @@ impl Editor {
 }
 
 impl EditorSnapshot {
+    pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
+        self.display_snapshot.buffer_snapshot.language_at(position)
+    }
+
     pub fn is_focused(&self) -> bool {
         self.is_focused
     }
@@ -6495,12 +6607,6 @@ impl<T> DerefMut for InvalidationStack<T> {
     }
 }
 
-impl InvalidationRegion for BracketPairState {
-    fn ranges(&self) -> &[Range<Anchor>] {
-        &self.ranges
-    }
-}
-
 impl InvalidationRegion for SnippetState {
     fn ranges(&self) -> &[Range<Anchor>] {
         &self.ranges[self.active_index]
@@ -6713,7 +6819,7 @@ mod tests {
         platform::{WindowBounds, WindowOptions},
     };
     use indoc::indoc;
-    use language::{FakeLspAdapter, LanguageConfig};
+    use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry};
     use project::FakeFs;
     use settings::EditorSettings;
     use std::{cell::RefCell, rc::Rc, time::Instant};

crates/editor/src/multi_buffer.rs 🔗

@@ -1228,9 +1228,9 @@ impl MultiBuffer {
         &self,
         point: T,
         cx: &'a AppContext,
-    ) -> Option<&'a Arc<Language>> {
+    ) -> Option<Arc<Language>> {
         self.point_to_buffer_offset(point, cx)
-            .and_then(|(buffer, _)| buffer.read(cx).language())
+            .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
     }
 
     pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
@@ -1966,6 +1966,24 @@ impl MultiBufferSnapshot {
         }
     }
 
+    pub fn point_to_buffer_offset<T: ToOffset>(
+        &self,
+        point: T,
+    ) -> Option<(&BufferSnapshot, usize)> {
+        let offset = point.to_offset(&self);
+        let mut cursor = self.excerpts.cursor::<usize>();
+        cursor.seek(&offset, Bias::Right, &());
+        if cursor.item().is_none() {
+            cursor.prev(&());
+        }
+
+        cursor.item().map(|excerpt| {
+            let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer);
+            let buffer_point = excerpt_start + offset - *cursor.start();
+            (&excerpt.buffer, buffer_point)
+        })
+    }
+
     pub fn suggested_indents(
         &self,
         rows: impl IntoIterator<Item = u32>,
@@ -1975,8 +1993,10 @@ impl MultiBufferSnapshot {
 
         let mut rows_for_excerpt = Vec::new();
         let mut cursor = self.excerpts.cursor::<Point>();
-
         let mut rows = rows.into_iter().peekable();
+        let mut prev_row = u32::MAX;
+        let mut prev_language_indent_size = IndentSize::default();
+
         while let Some(row) = rows.next() {
             cursor.seek(&Point::new(row, 0), Bias::Right, &());
             let excerpt = match cursor.item() {
@@ -1984,7 +2004,17 @@ impl MultiBufferSnapshot {
                 _ => continue,
             };
 
-            let single_indent_size = excerpt.buffer.single_indent_size(cx);
+            // Retrieve the language and indent size once for each disjoint region being indented.
+            let single_indent_size = if row.saturating_sub(1) == prev_row {
+                prev_language_indent_size
+            } else {
+                excerpt
+                    .buffer
+                    .language_indent_size_at(Point::new(row, 0), cx)
+            };
+            prev_language_indent_size = single_indent_size;
+            prev_row = row;
+
             let start_buffer_row = excerpt.range.context.start.to_point(&excerpt.buffer).row;
             let start_multibuffer_row = cursor.start().row;
 
@@ -2513,11 +2543,9 @@ impl MultiBufferSnapshot {
         self.trailing_excerpt_update_count
     }
 
-    pub fn language(&self) -> Option<&Arc<Language>> {
-        self.excerpts
-            .iter()
-            .next()
-            .and_then(|excerpt| excerpt.buffer.language())
+    pub fn language_at<'a, T: ToOffset>(&'a self, point: T) -> Option<&'a Arc<Language>> {
+        self.point_to_buffer_offset(point)
+            .and_then(|(buffer, offset)| buffer.language_at(offset))
     }
 
     pub fn is_dirty(&self) -> bool {

crates/language/Cargo.toml 🔗

@@ -64,6 +64,8 @@ util = { path = "../util", features = ["test-support"] }
 ctor = "0.1"
 env_logger = "0.9"
 rand = "0.8.3"
+tree-sitter-html = "*"
+tree-sitter-javascript = "*"
 tree-sitter-json = "*"
 tree-sitter-rust = "*"
 tree-sitter-python = "*"

crates/language/src/buffer.rs 🔗

@@ -95,14 +95,15 @@ pub struct BufferSnapshot {
     parse_count: usize,
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
 pub struct IndentSize {
     pub len: u32,
     pub kind: IndentKind,
 }
 
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
 pub enum IndentKind {
+    #[default]
     Space,
     Tab,
 }
@@ -247,7 +248,6 @@ pub enum AutoindentMode {
 struct AutoindentRequest {
     before_edit: BufferSnapshot,
     entries: Vec<AutoindentRequestEntry>,
-    indent_size: IndentSize,
     is_block_mode: bool,
 }
 
@@ -260,6 +260,7 @@ struct AutoindentRequestEntry {
     /// only be adjusted if the suggested indentation level has *changed*
     /// since the edit was made.
     first_line_is_new: bool,
+    indent_size: IndentSize,
     original_indent_column: Option<u32>,
 }
 
@@ -719,6 +720,16 @@ impl Buffer {
         self.language.as_ref()
     }
 
+    pub fn language_at<D: ToOffset>(&self, position: D) -> Option<Arc<Language>> {
+        let offset = position.to_offset(self);
+        self.syntax_map
+            .lock()
+            .layers_for_range(offset..offset, &self.text)
+            .last()
+            .map(|info| info.language.clone())
+            .or_else(|| self.language.clone())
+    }
+
     pub fn parse_count(&self) -> usize {
         self.parse_count
     }
@@ -866,10 +877,13 @@ impl Buffer {
                 // buffer before this batch of edits.
                 let mut row_ranges = Vec::new();
                 let mut old_to_new_rows = BTreeMap::new();
+                let mut language_indent_sizes_by_new_row = Vec::new();
                 for entry in &request.entries {
                     let position = entry.range.start;
                     let new_row = position.to_point(&snapshot).row;
                     let new_end_row = entry.range.end.to_point(&snapshot).row + 1;
+                    language_indent_sizes_by_new_row.push((new_row, entry.indent_size));
+
                     if !entry.first_line_is_new {
                         let old_row = position.to_point(&request.before_edit).row;
                         old_to_new_rows.insert(old_row, new_row);
@@ -883,6 +897,8 @@ impl Buffer {
                 let mut old_suggestions = BTreeMap::<u32, IndentSize>::default();
                 let old_edited_ranges =
                     contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields);
+                let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
+                let mut language_indent_size = IndentSize::default();
                 for old_edited_range in old_edited_ranges {
                     let suggestions = request
                         .before_edit
@@ -891,6 +907,17 @@ impl Buffer {
                         .flatten();
                     for (old_row, suggestion) in old_edited_range.zip(suggestions) {
                         if let Some(suggestion) = suggestion {
+                            let new_row = *old_to_new_rows.get(&old_row).unwrap();
+
+                            // Find the indent size based on the language for this row.
+                            while let Some((row, size)) = language_indent_sizes.peek() {
+                                if *row > new_row {
+                                    break;
+                                }
+                                language_indent_size = *size;
+                                language_indent_sizes.next();
+                            }
+
                             let suggested_indent = old_to_new_rows
                                 .get(&suggestion.basis_row)
                                 .and_then(|from_row| old_suggestions.get(from_row).copied())
@@ -899,9 +926,8 @@ impl Buffer {
                                         .before_edit
                                         .indent_size_for_line(suggestion.basis_row)
                                 })
-                                .with_delta(suggestion.delta, request.indent_size);
-                            old_suggestions
-                                .insert(*old_to_new_rows.get(&old_row).unwrap(), suggested_indent);
+                                .with_delta(suggestion.delta, language_indent_size);
+                            old_suggestions.insert(new_row, suggested_indent);
                         }
                     }
                     yield_now().await;
@@ -922,6 +948,8 @@ impl Buffer {
 
                 // Compute new suggestions for each line, but only include them in the result
                 // if they differ from the old suggestion for that line.
+                let mut language_indent_sizes = language_indent_sizes_by_new_row.iter().peekable();
+                let mut language_indent_size = IndentSize::default();
                 for new_edited_row_range in new_edited_row_ranges {
                     let suggestions = snapshot
                         .suggest_autoindents(new_edited_row_range.clone())
@@ -929,13 +957,22 @@ impl Buffer {
                         .flatten();
                     for (new_row, suggestion) in new_edited_row_range.zip(suggestions) {
                         if let Some(suggestion) = suggestion {
+                            // Find the indent size based on the language for this row.
+                            while let Some((row, size)) = language_indent_sizes.peek() {
+                                if *row > new_row {
+                                    break;
+                                }
+                                language_indent_size = *size;
+                                language_indent_sizes.next();
+                            }
+
                             let suggested_indent = indent_sizes
                                 .get(&suggestion.basis_row)
                                 .copied()
                                 .unwrap_or_else(|| {
                                     snapshot.indent_size_for_line(suggestion.basis_row)
                                 })
-                                .with_delta(suggestion.delta, request.indent_size);
+                                .with_delta(suggestion.delta, language_indent_size);
                             if old_suggestions
                                 .get(&new_row)
                                 .map_or(true, |old_indentation| {
@@ -1266,7 +1303,6 @@ impl Buffer {
         let edit_id = edit_operation.local_timestamp();
 
         if let Some((before_edit, mode)) = autoindent_request {
-            let indent_size = before_edit.single_indent_size(cx);
             let (start_columns, is_block_mode) = match mode {
                 AutoindentMode::Block {
                     original_indent_columns: start_columns,
@@ -1315,6 +1351,7 @@ impl Buffer {
                     AutoindentRequestEntry {
                         first_line_is_new,
                         original_indent_column: start_column,
+                        indent_size: before_edit.language_indent_size_at(range.start, cx),
                         range: self.anchor_before(new_start + range_of_insertion_to_indent.start)
                             ..self.anchor_after(new_start + range_of_insertion_to_indent.end),
                     }
@@ -1324,7 +1361,6 @@ impl Buffer {
             self.autoindent_requests.push(Arc::new(AutoindentRequest {
                 before_edit,
                 entries,
-                indent_size,
                 is_block_mode,
             }));
         }
@@ -1642,8 +1678,8 @@ impl BufferSnapshot {
         indent_size_for_line(self, row)
     }
 
-    pub fn single_indent_size(&self, cx: &AppContext) -> IndentSize {
-        let language_name = self.language().map(|language| language.name());
+    pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
+        let language_name = self.language_at(position).map(|language| language.name());
         let settings = cx.global::<Settings>();
         if settings.hard_tabs(language_name.as_deref()) {
             IndentSize::tab()
@@ -1713,6 +1749,8 @@ impl BufferSnapshot {
                 if capture.index == config.indent_capture_ix {
                     start.get_or_insert(Point::from_ts_point(capture.node.start_position()));
                     end.get_or_insert(Point::from_ts_point(capture.node.end_position()));
+                } else if Some(capture.index) == config.start_capture_ix {
+                    start = Some(Point::from_ts_point(capture.node.end_position()));
                 } else if Some(capture.index) == config.end_capture_ix {
                     end = Some(Point::from_ts_point(capture.node.start_position()));
                 }
@@ -1902,8 +1940,14 @@ impl BufferSnapshot {
         }
     }
 
-    pub fn language(&self) -> Option<&Arc<Language>> {
-        self.language.as_ref()
+    pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
+        let offset = position.to_offset(self);
+        self.syntax
+            .layers_for_range(offset..offset, &self.text)
+            .filter(|l| l.node.end_byte() > offset)
+            .last()
+            .map(|info| info.language)
+            .or(self.language.as_ref())
     }
 
     pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) {
@@ -1938,8 +1982,8 @@ impl BufferSnapshot {
     pub fn range_for_syntax_ancestor<T: ToOffset>(&self, range: Range<T>) -> Option<Range<usize>> {
         let range = range.start.to_offset(self)..range.end.to_offset(self);
         let mut result: Option<Range<usize>> = None;
-        'outer: for (_, _, node) in self.syntax.layers_for_range(range.clone(), &self.text) {
-            let mut cursor = node.walk();
+        'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) {
+            let mut cursor = layer.node.walk();
 
             // Descend to the first leaf that touches the start of the range,
             // and if the range is non-empty, extends beyond the start.

crates/language/src/language.rs 🔗

@@ -26,6 +26,7 @@ use serde_json::Value;
 use std::{
     any::Any,
     cell::RefCell,
+    fmt::Debug,
     mem,
     ops::Range,
     path::{Path, PathBuf},
@@ -135,7 +136,7 @@ impl CachedLspAdapter {
     pub async fn label_for_completion(
         &self,
         completion_item: &lsp::CompletionItem,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         self.adapter
             .label_for_completion(completion_item, language)
@@ -146,7 +147,7 @@ impl CachedLspAdapter {
         &self,
         name: &str,
         kind: lsp::SymbolKind,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         self.adapter.label_for_symbol(name, kind, language).await
     }
@@ -175,7 +176,7 @@ pub trait LspAdapter: 'static + Send + Sync {
     async fn label_for_completion(
         &self,
         _: &lsp::CompletionItem,
-        _: &Language,
+        _: &Arc<Language>,
     ) -> Option<CodeLabel> {
         None
     }
@@ -184,7 +185,7 @@ pub trait LspAdapter: 'static + Send + Sync {
         &self,
         _: &str,
         _: lsp::SymbolKind,
-        _: &Language,
+        _: &Arc<Language>,
     ) -> Option<CodeLabel> {
         None
     }
@@ -230,7 +231,10 @@ pub struct LanguageConfig {
     pub decrease_indent_pattern: Option<Regex>,
     #[serde(default)]
     pub autoclose_before: String,
-    pub line_comment: Option<String>,
+    #[serde(default)]
+    pub line_comment: Option<Arc<str>>,
+    #[serde(default)]
+    pub block_comment: Option<(Arc<str>, Arc<str>)>,
 }
 
 impl Default for LanguageConfig {
@@ -244,6 +248,7 @@ impl Default for LanguageConfig {
             decrease_indent_pattern: Default::default(),
             autoclose_before: Default::default(),
             line_comment: Default::default(),
+            block_comment: Default::default(),
         }
     }
 }
@@ -270,7 +275,7 @@ pub struct FakeLspAdapter {
     pub disk_based_diagnostics_sources: Vec<String>,
 }
 
-#[derive(Clone, Debug, Deserialize)]
+#[derive(Clone, Debug, Default, Deserialize)]
 pub struct BracketPair {
     pub start: String,
     pub end: String,
@@ -304,6 +309,7 @@ pub struct Grammar {
 struct IndentConfig {
     query: Query,
     indent_capture_ix: u32,
+    start_capture_ix: Option<u32>,
     end_capture_ix: Option<u32>,
 }
 
@@ -661,11 +667,13 @@ impl Language {
         let grammar = self.grammar_mut();
         let query = Query::new(grammar.ts_language, source)?;
         let mut indent_capture_ix = None;
+        let mut start_capture_ix = None;
         let mut end_capture_ix = None;
         get_capture_indices(
             &query,
             &mut [
                 ("indent", &mut indent_capture_ix),
+                ("start", &mut start_capture_ix),
                 ("end", &mut end_capture_ix),
             ],
         );
@@ -673,6 +681,7 @@ impl Language {
             grammar.indents_config = Some(IndentConfig {
                 query,
                 indent_capture_ix,
+                start_capture_ix,
                 end_capture_ix,
             });
         }
@@ -763,8 +772,15 @@ impl Language {
         self.config.name.clone()
     }
 
-    pub fn line_comment_prefix(&self) -> Option<&str> {
-        self.config.line_comment.as_deref()
+    pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
+        self.config.line_comment.as_ref()
+    }
+
+    pub fn block_comment_delimiters(&self) -> Option<(&Arc<str>, &Arc<str>)> {
+        self.config
+            .block_comment
+            .as_ref()
+            .map(|(start, end)| (start, end))
     }
 
     pub async fn disk_based_diagnostic_sources(&self) -> &[String] {
@@ -789,7 +805,7 @@ impl Language {
     }
 
     pub async fn label_for_completion(
-        &self,
+        self: &Arc<Self>,
         completion: &lsp::CompletionItem,
     ) -> Option<CodeLabel> {
         self.adapter
@@ -798,7 +814,11 @@ impl Language {
             .await
     }
 
-    pub async fn label_for_symbol(&self, name: &str, kind: lsp::SymbolKind) -> Option<CodeLabel> {
+    pub async fn label_for_symbol(
+        self: &Arc<Self>,
+        name: &str,
+        kind: lsp::SymbolKind,
+    ) -> Option<CodeLabel> {
         self.adapter
             .as_ref()?
             .label_for_symbol(name, kind, self)
@@ -806,20 +826,17 @@ impl Language {
     }
 
     pub fn highlight_text<'a>(
-        &'a self,
+        self: &'a Arc<Self>,
         text: &'a Rope,
         range: Range<usize>,
     ) -> Vec<(Range<usize>, HighlightId)> {
         let mut result = Vec::new();
         if let Some(grammar) = &self.grammar {
             let tree = grammar.parse_text(text, None);
-            let captures = SyntaxSnapshot::single_tree_captures(
-                range.clone(),
-                text,
-                &tree,
-                grammar,
-                |grammar| grammar.highlights_query.as_ref(),
-            );
+            let captures =
+                SyntaxSnapshot::single_tree_captures(range.clone(), text, &tree, self, |grammar| {
+                    grammar.highlights_query.as_ref()
+                });
             let highlight_maps = vec![grammar.highlight_map()];
             let mut offset = 0;
             for chunk in BufferChunks::new(text, range, Some((captures, highlight_maps)), vec![]) {
@@ -861,6 +878,14 @@ impl Language {
     }
 }
 
+impl Debug for Language {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Language")
+            .field("name", &self.config.name)
+            .finish()
+    }
+}
+
 impl Grammar {
     pub fn id(&self) -> usize {
         self.id

crates/language/src/syntax_map.rs 🔗

@@ -92,6 +92,13 @@ struct SyntaxLayer {
     language: Arc<Language>,
 }
 
+#[derive(Debug)]
+pub struct SyntaxLayerInfo<'a> {
+    pub depth: usize,
+    pub node: Node<'a>,
+    pub language: &'a Arc<Language>,
+}
+
 #[derive(Debug, Clone)]
 struct SyntaxLayerSummary {
     min_depth: usize,
@@ -473,13 +480,18 @@ impl SyntaxSnapshot {
         range: Range<usize>,
         text: &'a Rope,
         tree: &'a Tree,
-        grammar: &'a Grammar,
+        language: &'a Arc<Language>,
         query: fn(&Grammar) -> Option<&Query>,
     ) -> SyntaxMapCaptures<'a> {
         SyntaxMapCaptures::new(
             range.clone(),
             text,
-            [(grammar, 0, tree.root_node())].into_iter(),
+            [SyntaxLayerInfo {
+                language,
+                depth: 0,
+                node: tree.root_node(),
+            }]
+            .into_iter(),
             query,
         )
     }
@@ -513,19 +525,19 @@ impl SyntaxSnapshot {
     }
 
     #[cfg(test)]
-    pub fn layers(&self, buffer: &BufferSnapshot) -> Vec<(&Grammar, usize, Node)> {
-        self.layers_for_range(0..buffer.len(), buffer)
+    pub fn layers<'a>(&'a self, buffer: &'a BufferSnapshot) -> Vec<SyntaxLayerInfo> {
+        self.layers_for_range(0..buffer.len(), buffer).collect()
     }
 
     pub fn layers_for_range<'a, T: ToOffset>(
-        &self,
+        &'a self,
         range: Range<T>,
-        buffer: &BufferSnapshot,
-    ) -> Vec<(&Grammar, usize, Node)> {
+        buffer: &'a BufferSnapshot,
+    ) -> impl 'a + Iterator<Item = SyntaxLayerInfo> {
         let start = buffer.anchor_before(range.start.to_offset(buffer));
         let end = buffer.anchor_after(range.end.to_offset(buffer));
 
-        let mut cursor = self.layers.filter::<_, ()>(|summary| {
+        let mut cursor = self.layers.filter::<_, ()>(move |summary| {
             if summary.max_depth > summary.min_depth {
                 true
             } else {
@@ -535,23 +547,26 @@ impl SyntaxSnapshot {
             }
         });
 
-        let mut result = Vec::new();
+        // let mut result = Vec::new();
         cursor.next(buffer);
-        while let Some(layer) = cursor.item() {
-            if let Some(grammar) = &layer.language.grammar {
-                result.push((
-                    grammar.as_ref(),
-                    layer.depth,
-                    layer.tree.root_node_with_offset(
+        std::iter::from_fn(move || {
+            if let Some(layer) = cursor.item() {
+                let info = SyntaxLayerInfo {
+                    language: &layer.language,
+                    depth: layer.depth,
+                    node: layer.tree.root_node_with_offset(
                         layer.range.start.to_offset(buffer),
                         layer.range.start.to_point(buffer).to_ts_point(),
                     ),
-                ));
+                };
+                cursor.next(buffer);
+                Some(info)
+            } else {
+                None
             }
-            cursor.next(buffer)
-        }
+        })
 
-        result
+        // result
     }
 }
 
@@ -559,7 +574,7 @@ impl<'a> SyntaxMapCaptures<'a> {
     fn new(
         range: Range<usize>,
         text: &'a Rope,
-        layers: impl Iterator<Item = (&'a Grammar, usize, Node<'a>)>,
+        layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
         query: fn(&Grammar) -> Option<&Query>,
     ) -> Self {
         let mut result = Self {
@@ -567,11 +582,19 @@ impl<'a> SyntaxMapCaptures<'a> {
             grammars: Vec::new(),
             active_layer_count: 0,
         };
-        for (grammar, depth, node) in layers {
-            let query = if let Some(query) = query(grammar) {
-                query
-            } else {
-                continue;
+        for SyntaxLayerInfo {
+            language,
+            depth,
+            node,
+        } in layers
+        {
+            let grammar = match &language.grammar {
+                Some(grammer) => grammer,
+                None => continue,
+            };
+            let query = match query(&grammar) {
+                Some(query) => query,
+                None => continue,
             };
 
             let mut query_cursor = QueryCursorHandle::new();
@@ -678,15 +701,23 @@ impl<'a> SyntaxMapMatches<'a> {
     fn new(
         range: Range<usize>,
         text: &'a Rope,
-        layers: impl Iterator<Item = (&'a Grammar, usize, Node<'a>)>,
+        layers: impl Iterator<Item = SyntaxLayerInfo<'a>>,
         query: fn(&Grammar) -> Option<&Query>,
     ) -> Self {
         let mut result = Self::default();
-        for (grammar, depth, node) in layers {
-            let query = if let Some(query) = query(grammar) {
-                query
-            } else {
-                continue;
+        for SyntaxLayerInfo {
+            language,
+            depth,
+            node,
+        } in layers
+        {
+            let grammar = match &language.grammar {
+                Some(grammer) => grammer,
+                None => continue,
+            };
+            let query = match query(&grammar) {
+                Some(query) => query,
+                None => continue,
             };
 
             let mut query_cursor = QueryCursorHandle::new();
@@ -1624,8 +1655,8 @@ mod tests {
         let reference_layers = reference_syntax_map.layers(&buffer);
         for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter())
         {
-            assert_eq!(edited_layer.2.to_sexp(), reference_layer.2.to_sexp());
-            assert_eq!(edited_layer.2.range(), reference_layer.2.range());
+            assert_eq!(edited_layer.node.to_sexp(), reference_layer.node.to_sexp());
+            assert_eq!(edited_layer.node.range(), reference_layer.node.range());
         }
     }
 
@@ -1770,13 +1801,13 @@ mod tests {
                 mutated_layers.into_iter().zip(reference_layers.into_iter())
             {
                 assert_eq!(
-                    edited_layer.2.to_sexp(),
-                    reference_layer.2.to_sexp(),
+                    edited_layer.node.to_sexp(),
+                    reference_layer.node.to_sexp(),
                     "different layer at step {i}"
                 );
                 assert_eq!(
-                    edited_layer.2.range(),
-                    reference_layer.2.range(),
+                    edited_layer.node.range(),
+                    reference_layer.node.range(),
                     "different layer at step {i}"
                 );
             }
@@ -1822,13 +1853,15 @@ mod tests {
         range: Range<Point>,
         expected_layers: &[&str],
     ) {
-        let layers = syntax_map.layers_for_range(range, &buffer);
+        let layers = syntax_map
+            .layers_for_range(range, &buffer)
+            .collect::<Vec<_>>();
         assert_eq!(
             layers.len(),
             expected_layers.len(),
             "wrong number of layers"
         );
-        for (i, ((_, _, node), expected_s_exp)) in
+        for (i, (SyntaxLayerInfo { node, .. }, expected_s_exp)) in
             layers.iter().zip(expected_layers.iter()).enumerate()
         {
             let actual_s_exp = node.to_sexp();

crates/language/src/tests.rs 🔗

@@ -14,7 +14,7 @@ use std::{
 };
 use text::network::Network;
 use unindent::Unindent as _;
-use util::post_inc;
+use util::{post_inc, test::marked_text_ranges};
 
 #[cfg(test)]
 #[ctor::ctor]
@@ -1035,6 +1035,120 @@ fn test_autoindent_language_without_indents_query(cx: &mut MutableAppContext) {
     });
 }
 
+#[gpui::test]
+fn test_autoindent_with_injected_languages(cx: &mut MutableAppContext) {
+    cx.set_global({
+        let mut settings = Settings::test(cx);
+        settings.language_overrides.extend([
+            (
+                "HTML".into(),
+                settings::EditorSettings {
+                    tab_size: Some(2.try_into().unwrap()),
+                    ..Default::default()
+                },
+            ),
+            (
+                "JavaScript".into(),
+                settings::EditorSettings {
+                    tab_size: Some(8.try_into().unwrap()),
+                    ..Default::default()
+                },
+            ),
+        ]);
+        settings
+    });
+
+    let html_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "HTML".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_html::language()),
+        )
+        .with_indents_query(
+            "
+            (element
+              (start_tag) @start
+              (end_tag)? @end) @indent
+            ",
+        )
+        .unwrap()
+        .with_injection_query(
+            r#"
+            (script_element
+                (raw_text) @content
+                (#set! "language" "javascript"))
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let javascript_language = Arc::new(
+        Language::new(
+            LanguageConfig {
+                name: "JavaScript".into(),
+                ..Default::default()
+            },
+            Some(tree_sitter_javascript::language()),
+        )
+        .with_indents_query(
+            r#"
+            (object "}" @end) @indent
+            "#,
+        )
+        .unwrap(),
+    );
+
+    let language_registry = Arc::new(LanguageRegistry::test());
+    language_registry.add(html_language.clone());
+    language_registry.add(javascript_language.clone());
+
+    cx.add_model(|cx| {
+        let (text, ranges) = marked_text_ranges(
+            &"
+                <div>ˇ
+                </div>
+                <script>
+                    init({ˇ
+                    })
+                </script>
+                <span>ˇ
+                </span>
+            "
+            .unindent(),
+            false,
+        );
+
+        let mut buffer = Buffer::new(0, text, cx);
+        buffer.set_language_registry(language_registry);
+        buffer.set_language(Some(html_language), cx);
+        buffer.edit(
+            ranges.into_iter().map(|range| (range, "\na")),
+            Some(AutoindentMode::EachLine),
+            cx,
+        );
+        assert_eq!(
+            buffer.text(),
+            "
+                <div>
+                  a
+                </div>
+                <script>
+                    init({
+                            a
+                    })
+                </script>
+                <span>
+                  a
+                </span>
+            "
+            .unindent()
+        );
+        buffer
+    });
+}
+
 #[gpui::test]
 fn test_serialization(cx: &mut gpui::MutableAppContext) {
     let mut now = Instant::now();
@@ -1449,7 +1563,7 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> Str
     buffer.read_with(cx, |buffer, _| {
         let snapshot = buffer.snapshot();
         let layers = snapshot.syntax.layers(buffer.as_text_snapshot());
-        layers[0].2.to_sexp()
+        layers[0].node.to_sexp()
     })
 }
 

crates/zed/Cargo.toml 🔗

@@ -92,6 +92,7 @@ toml = "0.5"
 tree-sitter = "0.20"
 tree-sitter-c = "0.20.1"
 tree-sitter-cpp = "0.20.0"
+tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" }
 tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "05e3631c6a0701c1fa518b0fee7be95a2ceef5e2" }
 tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" }
 tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "137e1ce6a02698fc246cdb9c6b886ed1de9a1ed8" }
@@ -100,6 +101,7 @@ tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown",
 tree-sitter-python = "0.20.2"
 tree-sitter-toml = { git = "https://github.com/tree-sitter/tree-sitter-toml", rev = "342d9be207c2dba869b9967124c679b5e6fd0ebe" }
 tree-sitter-typescript = "0.20.1"
+tree-sitter-html = "0.19.0"
 url = "2.2"
 
 [dev-dependencies]

crates/zed/src/languages.rs 🔗

@@ -7,6 +7,7 @@ use std::{borrow::Cow, str, sync::Arc};
 mod c;
 mod elixir;
 mod go;
+mod html;
 mod installation;
 mod json;
 mod language_plugin;
@@ -46,6 +47,11 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
             tree_sitter_cpp::language(),
             Some(CachedLspAdapter::new(c::CLspAdapter).await),
         ),
+        (
+            "css",
+            tree_sitter_css::language(),
+            None, //
+        ),
         (
             "elixir",
             tree_sitter_elixir::language(),
@@ -96,8 +102,13 @@ pub async fn init(languages: Arc<LanguageRegistry>, _executor: Arc<Background>)
             tree_sitter_typescript::language_tsx(),
             Some(CachedLspAdapter::new(typescript::TypeScriptLspAdapter).await),
         ),
+        (
+            "html",
+            tree_sitter_html::language(),
+            Some(CachedLspAdapter::new(html::HtmlLspAdapter).await),
+        ),
     ] {
-        languages.add(Arc::new(language(name, grammar, lsp_adapter)));
+        languages.add(language(name, grammar, lsp_adapter));
     }
 }
 
@@ -105,7 +116,7 @@ pub(crate) fn language(
     name: &str,
     grammar: tree_sitter::Language,
     lsp_adapter: Option<Arc<CachedLspAdapter>>,
-) -> Language {
+) -> Arc<Language> {
     let config = toml::from_slice(
         &LanguageDir::get(&format!("{}/config.toml", name))
             .unwrap()
@@ -142,7 +153,7 @@ pub(crate) fn language(
     if let Some(lsp_adapter) = lsp_adapter {
         language = language.with_lsp_adapter(lsp_adapter)
     }
-    language
+    Arc::new(language)
 }
 
 fn load_query(name: &str, filename_prefix: &str) -> Option<Cow<'static, str>> {

crates/zed/src/languages/c.rs 🔗

@@ -112,7 +112,7 @@ impl super::LspAdapter for CLspAdapter {
     async fn label_for_completion(
         &self,
         completion: &lsp::CompletionItem,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         let label = completion
             .label
@@ -190,7 +190,7 @@ impl super::LspAdapter for CLspAdapter {
         &self,
         name: &str,
         kind: lsp::SymbolKind,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         let (text, filter_range, display_range) = match kind {
             lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
@@ -251,7 +251,6 @@ mod tests {
     use gpui::MutableAppContext;
     use language::{AutoindentMode, Buffer};
     use settings::Settings;
-    use std::sync::Arc;
 
     #[gpui::test]
     fn test_c_autoindent(cx: &mut MutableAppContext) {
@@ -262,7 +261,7 @@ mod tests {
         let language = crate::languages::language("c", tree_sitter_c::language(), None);
 
         cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
+            let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
 
             // empty function
             buffer.edit([(0..0, "int main() {}")], None, cx);

crates/zed/src/languages/css/config.toml 🔗

@@ -0,0 +1,9 @@
+name = "CSS"
+path_suffixes = ["css"]
+autoclose_before = ";:.,=}])>"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false }
+]

crates/zed/src/languages/css/highlights.scm 🔗

@@ -0,0 +1,76 @@
+(comment) @comment
+
+[
+  (tag_name)
+  (nesting_selector)
+  (universal_selector)
+] @tag
+
+[
+  "~"
+  ">"
+  "+"
+  "-"
+  "*"
+  "/"
+  "="
+  "^="
+  "|="
+  "~="
+  "$="
+  "*="
+  "and"
+  "or"
+  "not"
+  "only"
+] @operator
+
+(attribute_selector (plain_value) @string)
+
+(attribute_name) @attribute
+(pseudo_element_selector (tag_name) @attribute)
+(pseudo_class_selector (class_name) @attribute)
+
+[
+  (class_name)
+  (id_name)
+  (namespace_name)
+  (property_name)
+  (feature_name)
+] @property
+
+(function_name) @function
+
+((property_name) @variable
+ (#match? @variable "^--"))
+((plain_value) @variable
+ (#match? @variable "^--"))
+
+[
+  "@media"
+  "@import"
+  "@charset"
+  "@namespace"
+  "@supports"
+  "@keyframes"
+  (at_keyword)
+  (to)
+  (from)
+  (important)
+]  @keyword
+
+(string_value) @string
+(color_value) @string.special
+
+[
+  (integer_value)
+  (float_value)
+] @number
+
+(unit) @type
+
+[
+  "#"
+  ","
+  ":"
+] @punctuation.delimiter

crates/zed/src/languages/elixir.rs 🔗

@@ -113,7 +113,7 @@ impl LspAdapter for ElixirLspAdapter {
     async fn label_for_completion(
         &self,
         completion: &lsp::CompletionItem,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         match completion.kind.zip(completion.detail.as_ref()) {
             Some((_, detail)) if detail.starts_with("(function)") => {
@@ -168,7 +168,7 @@ impl LspAdapter for ElixirLspAdapter {
         &self,
         name: &str,
         kind: SymbolKind,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         let (text, filter_range, display_range) = match kind {
             SymbolKind::METHOD | SymbolKind::FUNCTION => {

crates/zed/src/languages/go.rs 🔗

@@ -134,7 +134,7 @@ impl super::LspAdapter for GoLspAdapter {
     async fn label_for_completion(
         &self,
         completion: &lsp::CompletionItem,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         let label = &completion.label;
 
@@ -235,7 +235,7 @@ impl super::LspAdapter for GoLspAdapter {
         &self,
         name: &str,
         kind: lsp::SymbolKind,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         let (text, filter_range, display_range) = match kind {
             lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {

crates/zed/src/languages/html.rs 🔗

@@ -0,0 +1,101 @@
+use super::installation::{npm_install_packages, npm_package_latest_version};
+use anyhow::{anyhow, Context, Result};
+use async_trait::async_trait;
+use client::http::HttpClient;
+use futures::StreamExt;
+use language::{LanguageServerName, LspAdapter};
+use serde_json::json;
+use smol::fs;
+use std::{any::Any, path::PathBuf, sync::Arc};
+use util::ResultExt;
+
+pub struct HtmlLspAdapter;
+
+impl HtmlLspAdapter {
+    const BIN_PATH: &'static str =
+        "node_modules/vscode-langservers-extracted/bin/vscode-html-language-server";
+}
+
+#[async_trait]
+impl LspAdapter for HtmlLspAdapter {
+    async fn name(&self) -> LanguageServerName {
+        LanguageServerName("vscode-html-language-server".into())
+    }
+
+    async fn server_args(&self) -> Vec<String> {
+        vec!["--stdio".into()]
+    }
+
+    async fn fetch_latest_server_version(
+        &self,
+        _: Arc<dyn HttpClient>,
+    ) -> Result<Box<dyn 'static + Any + Send>> {
+        Ok(Box::new(npm_package_latest_version("vscode-langservers-extracted").await?) as Box<_>)
+    }
+
+    async fn fetch_server_binary(
+        &self,
+        version: Box<dyn 'static + Send + Any>,
+        _: Arc<dyn HttpClient>,
+        container_dir: PathBuf,
+    ) -> Result<PathBuf> {
+        let version = version.downcast::<String>().unwrap();
+        let version_dir = container_dir.join(version.as_str());
+        fs::create_dir_all(&version_dir)
+            .await
+            .context("failed to create version directory")?;
+        let binary_path = version_dir.join(Self::BIN_PATH);
+
+        if fs::metadata(&binary_path).await.is_err() {
+            npm_install_packages(
+                [("vscode-langservers-extracted", version.as_str())],
+                &version_dir,
+            )
+            .await?;
+
+            if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() {
+                while let Some(entry) = entries.next().await {
+                    if let Some(entry) = entry.log_err() {
+                        let entry_path = entry.path();
+                        if entry_path.as_path() != version_dir {
+                            fs::remove_dir_all(&entry_path).await.log_err();
+                        }
+                    }
+                }
+            }
+        }
+
+        Ok(binary_path)
+    }
+
+    async fn cached_server_binary(&self, container_dir: PathBuf) -> Option<PathBuf> {
+        (|| async move {
+            let mut last_version_dir = None;
+            let mut entries = fs::read_dir(&container_dir).await?;
+            while let Some(entry) = entries.next().await {
+                let entry = entry?;
+                if entry.file_type().await?.is_dir() {
+                    last_version_dir = Some(entry.path());
+                }
+            }
+            let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?;
+            let bin_path = last_version_dir.join(Self::BIN_PATH);
+            if bin_path.exists() {
+                Ok(bin_path)
+            } else {
+                Err(anyhow!(
+                    "missing executable in directory {:?}",
+                    last_version_dir
+                ))
+            }
+        })()
+        .await
+        .log_err()
+    }
+
+    async fn initialization_options(&self) -> Option<serde_json::Value> {
+        Some(json!({
+            "provideFormatter": true
+        }))
+    }
+}

crates/zed/src/languages/html/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "HTML"
+path_suffixes = ["html"]
+autoclose_before = ">})"
+brackets = [
+    { start = "<", end = ">", close = true, newline = true },
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "(", end = ")", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false },
+    { start = "!--", end = " --", close = true, newline = false },
+]
+
+block_comment = ["<!-- ", " -->"]

crates/zed/src/languages/html/highlights.scm 🔗

@@ -0,0 +1,15 @@
+(tag_name) @keyword
+(erroneous_end_tag_name) @keyword
+(doctype) @constant
+(attribute_name) @property
+(attribute_value) @string
+(comment) @comment
+
+"=" @operator
+
+[
+  "<"
+  ">"
+  "</"
+  "/>"
+] @punctuation.bracket

crates/zed/src/languages/python.rs 🔗

@@ -90,7 +90,7 @@ impl LspAdapter for PythonLspAdapter {
     async fn label_for_completion(
         &self,
         item: &lsp::CompletionItem,
-        language: &language::Language,
+        language: &Arc<language::Language>,
     ) -> Option<language::CodeLabel> {
         let label = &item.label;
         let grammar = language.grammar()?;
@@ -112,7 +112,7 @@ impl LspAdapter for PythonLspAdapter {
         &self,
         name: &str,
         kind: lsp::SymbolKind,
-        language: &language::Language,
+        language: &Arc<language::Language>,
     ) -> Option<language::CodeLabel> {
         let (text, filter_range, display_range) = match kind {
             lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
@@ -149,7 +149,6 @@ mod tests {
     use gpui::{ModelContext, MutableAppContext};
     use language::{AutoindentMode, Buffer};
     use settings::Settings;
-    use std::sync::Arc;
 
     #[gpui::test]
     fn test_python_autoindent(cx: &mut MutableAppContext) {
@@ -160,7 +159,7 @@ mod tests {
         cx.set_global(settings);
 
         cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
+            let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
             let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext<Buffer>| {
                 let ix = buffer.len();
                 buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);

crates/zed/src/languages/rust.rs 🔗

@@ -119,7 +119,7 @@ impl LspAdapter for RustLspAdapter {
     async fn label_for_completion(
         &self,
         completion: &lsp::CompletionItem,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         match completion.kind {
             Some(lsp::CompletionItemKind::FIELD) if completion.detail.is_some() => {
@@ -196,7 +196,7 @@ impl LspAdapter for RustLspAdapter {
         &self,
         name: &str,
         kind: lsp::SymbolKind,
-        language: &Language,
+        language: &Arc<Language>,
     ) -> Option<CodeLabel> {
         let (text, filter_range, display_range) = match kind {
             lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
@@ -439,7 +439,7 @@ mod tests {
         cx.set_global(settings);
 
         cx.add_model(|cx| {
-            let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(language), cx);
+            let mut buffer = Buffer::new(0, "", cx).with_language(language, cx);
 
             // indent between braces
             buffer.set_text("fn a() {}", cx);

crates/zed/src/languages/typescript.rs 🔗

@@ -115,7 +115,7 @@ impl LspAdapter for TypeScriptLspAdapter {
     async fn label_for_completion(
         &self,
         item: &lsp::CompletionItem,
-        language: &language::Language,
+        language: &Arc<language::Language>,
     ) -> Option<language::CodeLabel> {
         use lsp::CompletionItemKind as Kind;
         let len = item.label.len();
@@ -144,7 +144,6 @@ impl LspAdapter for TypeScriptLspAdapter {
 
 #[cfg(test)]
 mod tests {
-    use std::sync::Arc;
 
     use gpui::MutableAppContext;
     use unindent::Unindent;
@@ -172,9 +171,8 @@ mod tests {
         "#
         .unindent();
 
-        let buffer = cx.add_model(|cx| {
-            language::Buffer::new(0, text, cx).with_language(Arc::new(language), cx)
-        });
+        let buffer =
+            cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx));
         let outline = buffer.read(cx).snapshot().outline(None).unwrap();
         assert_eq!(
             outline

crates/zed/src/zed.rs 🔗

@@ -1133,7 +1133,7 @@ mod tests {
             assert!(!editor.is_dirty(cx));
             assert_eq!(editor.title(cx), "untitled");
             assert!(Arc::ptr_eq(
-                editor.language_at(0, cx).unwrap(),
+                &editor.language_at(0, cx).unwrap(),
                 &languages::PLAIN_TEXT
             ));
             editor.handle_input("hi", cx);
@@ -1220,7 +1220,7 @@ mod tests {
 
         editor.update(cx, |editor, cx| {
             assert!(Arc::ptr_eq(
-                editor.language_at(0, cx).unwrap(),
+                &editor.language_at(0, cx).unwrap(),
                 &languages::PLAIN_TEXT
             ));
             editor.handle_input("hi", cx);