diff --git a/Cargo.lock b/Cargo.lock index 13f64e2a2c..c6fbe523b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3009,6 +3009,7 @@ dependencies = [ "tree-sitter-javascript", "tree-sitter-json 0.19.0", "tree-sitter-python", + "tree-sitter-ruby", "tree-sitter-rust", "tree-sitter-typescript", "unindent", @@ -6491,6 +6492,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-ruby" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ac30cbb1560363ae76e1ccde543d6d99087421e228cc47afcec004b86bb711a" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-rust" version = "0.20.3" @@ -7712,6 +7723,7 @@ dependencies = [ "tree-sitter-json 0.20.0", "tree-sitter-markdown", "tree-sitter-python", + "tree-sitter-ruby", "tree-sitter-rust", "tree-sitter-toml", "tree-sitter-typescript", diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 9a11b80511..419c7a79a5 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -71,4 +71,5 @@ tree-sitter-json = "*" tree-sitter-rust = "*" tree-sitter-python = "*" tree-sitter-typescript = "*" +tree-sitter-ruby = "*" unindent = "0.1.7" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0756b90c36..2a0b158a7a 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1764,6 +1764,7 @@ impl BufferSnapshot { .collect::>(); let mut indent_ranges = Vec::>::new(); + let mut outdent_positions = Vec::::new(); while let Some(mat) = matches.peek() { let mut start: Option = None; let mut end: Option = None; @@ -1777,6 +1778,8 @@ impl BufferSnapshot { 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())); + } else if Some(capture.index) == config.outdent_capture_ix { + outdent_positions.push(Point::from_ts_point(capture.node.start_position())); } } @@ -1797,6 +1800,19 @@ impl BufferSnapshot { } } + outdent_positions.sort(); + for outdent_position in outdent_positions { + // find the innermost indent range containing this outdent_position + // set its end to the outdent position + if let Some(range_to_truncate) = indent_ranges + .iter_mut() + .filter(|indent_range| indent_range.contains(&outdent_position)) + .last() + { + range_to_truncate.end = outdent_position; + } + } + // Find the suggested indentation increases and decreased based on regexes. let mut indent_change_rows = Vec::<(u32, Ordering)>::new(); self.for_each_line( diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index f1b51f7e02..6043127dd5 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -1150,6 +1150,49 @@ fn test_autoindent_with_injected_languages(cx: &mut MutableAppContext) { }); } +#[gpui::test] +fn test_autoindent_query_with_outdent_captures(cx: &mut MutableAppContext) { + let mut settings = Settings::test(cx); + settings.editor_defaults.tab_size = Some(2.try_into().unwrap()); + cx.set_global(settings); + cx.add_model(|cx| { + let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx); + + let text = r#" + class C + def a(b, c) + puts b + puts c + rescue + puts "errored" + exit 1 + end + end + "# + .unindent(); + + buffer.edit([(0..0, text)], Some(AutoindentMode::EachLine), cx); + + assert_eq!( + buffer.text(), + r#" + class C + def a(b, c) + puts b + puts c + rescue + puts "errored" + exit 1 + end + end + "# + .unindent() + ); + + buffer + }); +} + #[gpui::test] fn test_serialization(cx: &mut gpui::MutableAppContext) { let mut now = Instant::now(); @@ -1497,6 +1540,26 @@ impl Buffer { } } +fn ruby_lang() -> Language { + Language::new( + LanguageConfig { + name: "Ruby".into(), + path_suffixes: vec!["rb".to_string()], + ..Default::default() + }, + Some(tree_sitter_ruby::language()), + ) + .with_indents_query( + r#" + (class "end" @end) @indent + (method "end" @end) @indent + (rescue) @outdent + (then) @indent + "#, + ) + .unwrap() +} + fn rust_lang() -> Language { Language::new( LanguageConfig { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index bb75edbc32..5abc89321c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -312,6 +312,7 @@ struct IndentConfig { indent_capture_ix: u32, start_capture_ix: Option, end_capture_ix: Option, + outdent_capture_ix: Option, } struct OutlineConfig { @@ -670,12 +671,14 @@ impl Language { let mut indent_capture_ix = None; let mut start_capture_ix = None; let mut end_capture_ix = None; + let mut outdent_capture_ix = None; get_capture_indices( &query, &mut [ ("indent", &mut indent_capture_ix), ("start", &mut start_capture_ix), ("end", &mut end_capture_ix), + ("outdent", &mut outdent_capture_ix), ], ); if let Some(indent_capture_ix) = indent_capture_ix { @@ -684,6 +687,7 @@ impl Language { indent_capture_ix, start_capture_ix, end_capture_ix, + outdent_capture_ix, }); } Ok(self) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 5b65073087..cca40cbe5a 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -102,6 +102,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-ruby = "0.20.0" tree-sitter-html = "0.19.0" url = "2.2" diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 2745fa824a..c1e17a8e1a 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -15,6 +15,15 @@ mod python; mod rust; mod typescript; +// 1. Add tree-sitter-{language} parser to zed crate +// 2. Create a language directory in zed/crates/zed/src/languages and add the language to init function below +// 3. Add config.toml to the newly created language directory using existing languages as a template +// 4. Copy highlights from tree sitter repo for the language into a highlights.scm file. +// Note: github highlights take the last match while zed takes the first +// 5. Add indents.scm, outline.scm, and brackets.scm to implement indent on newline, outline/breadcrumbs, +// and autoclosing brackets respectively +// 6. If the language has injections add an injections.scm query file + #[derive(RustEmbed)] #[folder = "src/languages"] #[exclude = "*.rs"] @@ -107,6 +116,7 @@ pub async fn init(languages: Arc, _executor: Arc) tree_sitter_html::language(), Some(CachedLspAdapter::new(html::HtmlLspAdapter).await), ), + ("ruby", tree_sitter_ruby::language(), None), ] { languages.add(language(name, grammar, lsp_adapter)); } diff --git a/crates/zed/src/languages/ruby/brackets.scm b/crates/zed/src/languages/ruby/brackets.scm new file mode 100644 index 0000000000..f5129f8f31 --- /dev/null +++ b/crates/zed/src/languages/ruby/brackets.scm @@ -0,0 +1,14 @@ +("[" @open "]" @close) +("{" @open "}" @close) +("\"" @open "\"" @close) +("do" @open "end" @close) + +(block_parameters "|" @open "|" @close) +(interpolation "#{" @open "}" @close) + +(if "if" @open "end" @close) +(unless "unless" @open "end" @close) +(begin "begin" @open "end" @close) +(module "module" @open "end" @close) +(_ . "def" @open "end" @close) +(_ . "class" @open "end" @close) diff --git a/crates/zed/src/languages/ruby/config.toml b/crates/zed/src/languages/ruby/config.toml new file mode 100644 index 0000000000..5600266de3 --- /dev/null +++ b/crates/zed/src/languages/ruby/config.toml @@ -0,0 +1,11 @@ +name = "Ruby" +path_suffixes = ["rb", "Gemfile"] +line_comment = "# " +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 = false, newline = false }, +] \ No newline at end of file diff --git a/crates/zed/src/languages/ruby/highlights.scm b/crates/zed/src/languages/ruby/highlights.scm new file mode 100644 index 0000000000..0dfd725cd8 --- /dev/null +++ b/crates/zed/src/languages/ruby/highlights.scm @@ -0,0 +1,181 @@ +; Keywords + +[ + "alias" + "and" + "begin" + "break" + "case" + "class" + "def" + "do" + "else" + "elsif" + "end" + "ensure" + "for" + "if" + "in" + "module" + "next" + "or" + "rescue" + "retry" + "return" + "then" + "unless" + "until" + "when" + "while" + "yield" +] @keyword + +(identifier) @variable + +((identifier) @keyword + (#match? @keyword "^(private|protected|public)$")) + +; Function calls + +((identifier) @function.method.builtin + (#eq? @function.method.builtin "require")) + +"defined?" @function.method.builtin + +(call + method: [(identifier) (constant)] @function.method) + +; Function definitions + +(alias (identifier) @function.method) +(setter (identifier) @function.method) +(method name: [(identifier) (constant)] @function.method) +(singleton_method name: [(identifier) (constant)] @function.method) + +; Identifiers + +[ + (class_variable) + (instance_variable) +] @property + +((identifier) @constant.builtin + (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) + +(file) @constant.builtin +(line) @constant.builtin +(encoding) @constant.builtin + +(hash_splat_nil + "**" @operator +) @constant.builtin + +((constant) @constant + (#match? @constant "^[A-Z\\d_]+$")) + +(constant) @type + +(self) @variable.special +(super) @variable.special + +; Literals + +[ + (string) + (bare_string) + (subshell) + (heredoc_body) + (heredoc_beginning) +] @string + +[ + (simple_symbol) + (delimited_symbol) + (hash_key_symbol) + (bare_symbol) +] @string.special.symbol + +(regex) @string.special.regex +(escape_sequence) @escape + +[ + (integer) + (float) +] @number + +[ + (nil) + (true) + (false) +] @constant.builtin + +(interpolation + "#{" @punctuation.special + "}" @punctuation.special) @embedded + +(comment) @comment + +; Operators + +[ + "!" + "~" + "+" + "-" + "**" + "*" + "/" + "%" + "<<" + ">>" + "&" + "|" + "^" + ">" + "<" + "<=" + ">=" + "==" + "!=" + "=~" + "!~" + "<=>" + "||" + "&&" + ".." + "..." + "=" + "**=" + "*=" + "/=" + "%=" + "+=" + "-=" + "<<=" + ">>=" + "&&=" + "&=" + "||=" + "|=" + "^=" + "=>" + "->" + (operator) +] @operator + +[ + "," + ";" + "." +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "%w(" + "%i(" +] @punctuation.bracket \ No newline at end of file diff --git a/crates/zed/src/languages/ruby/indents.scm b/crates/zed/src/languages/ruby/indents.scm new file mode 100644 index 0000000000..ac5175fa6f --- /dev/null +++ b/crates/zed/src/languages/ruby/indents.scm @@ -0,0 +1,17 @@ +(method "end" @end) @indent +(class "end" @end) @indent +(module "end" @end) @indent +(begin "end" @end) @indent +(do_block "end" @end) @indent + +(then) @indent +(call) @indent + +(ensure) @outdent +(rescue) @outdent +(else) @outdent + + +(_ "[" "]" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed/src/languages/ruby/outline.scm b/crates/zed/src/languages/ruby/outline.scm new file mode 100644 index 0000000000..e549a2d194 --- /dev/null +++ b/crates/zed/src/languages/ruby/outline.scm @@ -0,0 +1,11 @@ +(class + "class" @context + name: (_) @name) @item + +(method + "def" @context + name: (_) @name) @item + +(module + "module" @context + name: (_) @name) @item \ No newline at end of file diff --git a/styles/src/styleTree/editor.ts b/styles/src/styleTree/editor.ts index 82e6dfff90..65aede48a5 100644 --- a/styles/src/styleTree/editor.ts +++ b/styles/src/styleTree/editor.ts @@ -1,10 +1,6 @@ import { fontWeights } from "../common"; import { withOpacity } from "../utils/color"; -import { - ColorScheme, - Layer, - StyleSets, -} from "../themes/common/colorScheme"; +import { ColorScheme, Layer, StyleSets } from "../themes/common/colorScheme"; import { background, border, @@ -50,6 +46,11 @@ export default function editor(colorScheme: ColorScheme) { color: colorScheme.ramps.neutral(1).hex(), weight: fontWeights.normal, }, + "variable.special": { + // Highlights for self, this, etc + color: colorScheme.ramps.blue(0.7).hex(), + weight: fontWeights.normal, + }, comment: { color: colorScheme.ramps.neutral(0.71).hex(), weight: fontWeights.normal, @@ -270,9 +271,9 @@ export default function editor(colorScheme: ColorScheme) { background: withOpacity(background(layer, "inverted"), 0.4), border: { width: 1, - color: borderColor(layer, 'variant'), + color: borderColor(layer, "variant"), }, - } + }, }, compositionMark: { underline: {