mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 00:07:29 +00:00
feat(languages): add ERB with an HTML + Ruby editor mode (#720)
Ruby gets an ERB editor mode (HTML with embedded Ruby), Ruby / Rake / RSpec run configuration types, and a Settings › Languages & Frameworks › Ruby page for choosing the interpreter (Auto picks the `.ruby-version` / mise / asdf install, else `ruby` on PATH). - Run: `.rb` scripts run from the editor context menu, `_spec.rb` / `_test.rb` files narrow an RSpec or Rake (`SPEC=` / `TEST=`) configuration, and Rakefile tasks are detected; Rake and RSpec go through `bundle exec` when the project has a Gemfile. The chosen interpreter's `ruby`, `bundle`, `rake` and `rspec` are used and its bin directory is put on PATH. - Configuration forms rebuild from a raw command line keeping option values with their options (`--format documentation`, `-I lib`), and a new Ruby configuration opens in the Ruby form with the script still to fill in. - External Libraries lists the Ruby standard library and the bundle's gems, including those Bundler installs inside the project (`vendor/bundle`). - Ruby is part of the frontend settings defaults and normalization, so the selected interpreter survives a settings round-trip. Closes #719 Co-authored-by: retsef <2175889+retsef@users.noreply.github.com>
This commit is contained in:
co-authored by
retsef
parent
fcfe26474d
commit
002401e8a7
@@ -548,6 +548,7 @@ pub struct LanguagesSettings {
|
||||
pub typescript: TypeScriptSettings,
|
||||
pub prettier: PrettierSettings,
|
||||
pub java: JavaSettings,
|
||||
pub ruby: RubySettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
||||
@@ -605,6 +606,13 @@ impl Default for PythonSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct RubySettings {
|
||||
/// Project interpreter executable; empty = auto-detect (`.ruby-version`, mise / asdf, else `ruby` on PATH).
|
||||
pub interpreter: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, Type)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct NodeSettings {
|
||||
|
||||
@@ -18,6 +18,7 @@ pub enum Tool {
|
||||
Go,
|
||||
Maven,
|
||||
Gradle,
|
||||
Ruby,
|
||||
}
|
||||
|
||||
impl Tool {
|
||||
@@ -30,6 +31,7 @@ impl Tool {
|
||||
Tool::Go => &["go", "golang"],
|
||||
Tool::Maven => &["maven"],
|
||||
Tool::Gradle => &["gradle"],
|
||||
Tool::Ruby => &["ruby"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const BUILTIN: &[(&str, &str)] = &[
|
||||
("dockerfile", include_str!("../../../languages/dockerfile.toml")),
|
||||
("dotenv", include_str!("../../../languages/dotenv.toml")),
|
||||
("elixir", include_str!("../../../languages/elixir.toml")),
|
||||
("erb", include_str!("../../../languages/erb.toml")),
|
||||
("erlang", include_str!("../../../languages/erlang.toml")),
|
||||
("fsharp", include_str!("../../../languages/fsharp.toml")),
|
||||
("gitattributes", include_str!("../../../languages/gitattributes.toml")),
|
||||
@@ -520,6 +521,7 @@ mod tests {
|
||||
"MsGenny",
|
||||
"Vue",
|
||||
"Angular Template",
|
||||
"ERB",
|
||||
];
|
||||
|
||||
fn reg() -> LanguageRegistry {
|
||||
@@ -776,6 +778,7 @@ mod tests {
|
||||
("AppDelegate.m", "objective-c"),
|
||||
("a.rb", "ruby"),
|
||||
("Gemfile", "ruby"),
|
||||
("show.html.erb", "erb"),
|
||||
("index.php", "php"),
|
||||
("init.lua", "lua"),
|
||||
("main.dart", "dart"),
|
||||
|
||||
@@ -33,6 +33,7 @@ pub(crate) fn detect_with_profiles(
|
||||
detect_maven(root, excluded_poms, &mut out);
|
||||
detect_go(root, &mut out);
|
||||
detect_python(root, &mut out);
|
||||
detect_ruby(root, &mut out);
|
||||
cmake::detect(root, profiles, &mut out);
|
||||
dotnet::detect(root, &mut out);
|
||||
detect_make(root, &mut out);
|
||||
@@ -905,6 +906,133 @@ fn detect_python(root: &Path, out: &mut Vec<RunConfig>) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------- ruby
|
||||
|
||||
/// Tasks a Rakefile defines statically: `task :name`, `task "name" => deps`,
|
||||
/// `task name: [...]`, qualified by enclosing `namespace` blocks. `desc` lines are
|
||||
/// skipped; a `Rake::TestTask` contributes `test`. `load`/`import`ed files are not followed.
|
||||
pub(crate) fn rake_tasks(src: &str) -> Vec<String> {
|
||||
let mut tasks = Vec::new();
|
||||
let mut namespaces: Vec<String> = Vec::new();
|
||||
let mut depth: Vec<bool> = Vec::new(); // per open block: whether it is a namespace
|
||||
let name_of = |rest: &str| -> Option<String> {
|
||||
let rest = rest.trim_start();
|
||||
let (name, _) = if let Some(r) = rest.strip_prefix(':') {
|
||||
r.split(|c: char| !(c.is_alphanumeric() || c == '_')).next().map(|n| (n.to_owned(), ()))?
|
||||
} else if let Some(r) = rest.strip_prefix(['"', '\'']) {
|
||||
r.split(['"', '\'']).next().map(|n| (n.to_owned(), ()))?
|
||||
} else {
|
||||
let n: String = rest.chars().take_while(|c| c.is_alphanumeric() || *c == '_').collect();
|
||||
(!n.is_empty() && rest[n.len()..].starts_with(':')).then_some((n, ()))?
|
||||
};
|
||||
(!name.is_empty()).then_some(name)
|
||||
};
|
||||
for raw in src.lines() {
|
||||
let line = raw.trim();
|
||||
if line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let opens = line.ends_with(" do") || line.contains(" do |") || line.contains(" do ") && !line.contains(" end");
|
||||
if let Some(rest) = line.strip_prefix("namespace ")
|
||||
&& let Some(ns) = name_of(rest)
|
||||
{
|
||||
namespaces.push(ns);
|
||||
depth.push(true);
|
||||
continue;
|
||||
}
|
||||
if let Some(rest) = line.strip_prefix("task ").or_else(|| line.strip_prefix("task("))
|
||||
&& let Some(name) = name_of(rest)
|
||||
{
|
||||
let full = namespaces.iter().chain([&name]).cloned().collect::<Vec<_>>().join(":");
|
||||
if !tasks.contains(&full) {
|
||||
tasks.push(full);
|
||||
}
|
||||
}
|
||||
if line.contains("Rake::TestTask.new") || line.contains("RSpec::Core::RakeTask.new") {
|
||||
let name = name_of(line.split_once(".new").map(|(_, r)| r.trim_start_matches(['(', ' '])).unwrap_or(""))
|
||||
.unwrap_or_else(|| if line.contains("RSpec") { "spec".into() } else { "test".into() });
|
||||
let full = namespaces.iter().chain([&name]).cloned().collect::<Vec<_>>().join(":");
|
||||
if !tasks.contains(&full) {
|
||||
tasks.push(full);
|
||||
}
|
||||
}
|
||||
if opens && !line.starts_with("namespace ") {
|
||||
depth.push(false);
|
||||
}
|
||||
if (line == "end" || line.starts_with("end ") || line.starts_with("end#")) && depth.pop() == Some(true) {
|
||||
namespaces.pop();
|
||||
}
|
||||
}
|
||||
tasks
|
||||
}
|
||||
|
||||
/// `bundle exec <tool>` when the project has a Gemfile, else the bare tool.
|
||||
fn ruby_tool(root: &Path, tool: &str) -> (String, Vec<String>) {
|
||||
if root.join("Gemfile").is_file() {
|
||||
("bundle".into(), vec!["exec".into(), tool.into()])
|
||||
} else {
|
||||
(tool.into(), Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn ruby_config(
|
||||
id: String,
|
||||
name: String,
|
||||
kind: &str,
|
||||
(command, mut args): (String, Vec<String>),
|
||||
extra: &[&str],
|
||||
) -> RunConfig {
|
||||
args.extend(extra.iter().map(|a| (*a).to_owned()));
|
||||
let mut c = config(id, name, kind, &command, &[]);
|
||||
c.args = args;
|
||||
c
|
||||
}
|
||||
|
||||
fn uses_rspec(root: &Path) -> bool {
|
||||
root.join("spec").is_dir()
|
||||
|| root.join(".rspec").is_file()
|
||||
|| read(&root.join("Gemfile")).is_some_and(|g| g.contains("rspec"))
|
||||
}
|
||||
|
||||
fn detect_ruby(root: &Path, out: &mut Vec<RunConfig>) {
|
||||
let markers = ["Gemfile", "Rakefile", "rakefile", "Rakefile.rb", ".ruby-version", "config.ru"];
|
||||
let gemspec = fs::read_dir(root)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flatten()
|
||||
.any(|e| e.path().extension().is_some_and(|x| x == "gemspec"));
|
||||
if !gemspec && !markers.iter().any(|m| root.join(m).is_file()) {
|
||||
return;
|
||||
}
|
||||
if root.join("bin/rails").is_file() {
|
||||
let mut c = ruby_config(
|
||||
"rails:server".into(),
|
||||
"rails server".into(),
|
||||
"ruby",
|
||||
("ruby".into(), Vec::new()),
|
||||
&["bin/rails", "server"],
|
||||
);
|
||||
c.extras.folder = Some("Rails".into());
|
||||
out.push(c);
|
||||
}
|
||||
if let Some(rakefile) = first_existing(root, &["Rakefile", "rakefile", "Rakefile.rb"])
|
||||
&& let Some(src) = read(&rakefile)
|
||||
{
|
||||
for task in rake_tasks(&src) {
|
||||
let mut c =
|
||||
ruby_config(format!("rake:{task}"), format!("rake {task}"), "rake", ruby_tool(root, "rake"), &[&task]);
|
||||
if task == "test" || task == "spec" || task.ends_with(":test") || task.ends_with(":spec") {
|
||||
c = c.test();
|
||||
}
|
||||
c.extras.folder = Some("Rake".into());
|
||||
out.push(c);
|
||||
}
|
||||
}
|
||||
if uses_rspec(root) {
|
||||
out.push(ruby_config("rspec".into(), "rspec".into(), "rspec", ruby_tool(root, "rspec"), &[]).test());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- make / just
|
||||
|
||||
pub(crate) fn make_targets(src: &str) -> Vec<String> {
|
||||
|
||||
@@ -728,3 +728,69 @@ fn cmake_profile_build_configures_only_without_a_cache() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_projects_list_rake_tasks_rspec_and_rails() {
|
||||
let rakefile = "\
|
||||
require 'rake/testtask'
|
||||
|
||||
desc \"Build it\"
|
||||
task :build do
|
||||
sh 'make'
|
||||
end
|
||||
task \"lint\" => [:build]
|
||||
task deploy: :build do |t|
|
||||
puts t
|
||||
end
|
||||
namespace :db do
|
||||
desc 'Migrate'
|
||||
task :migrate, [:version] do |t, args|
|
||||
puts args
|
||||
end
|
||||
namespace :schema do
|
||||
task :load
|
||||
end
|
||||
end
|
||||
Rake::TestTask.new do |t|
|
||||
t.libs << 'test'
|
||||
end
|
||||
task default: :build
|
||||
";
|
||||
let dir = project(&[
|
||||
("Gemfile", "source 'https://rubygems.org'\ngem 'rspec'\n"),
|
||||
("Rakefile", rakefile),
|
||||
("bin/rails", "#!/usr/bin/env ruby\n"),
|
||||
("spec/a_spec.rb", ""),
|
||||
]);
|
||||
let c = detect(dir.path());
|
||||
assert_eq!(
|
||||
ids(&c),
|
||||
[
|
||||
"rails:server",
|
||||
"rake:build",
|
||||
"rake:lint",
|
||||
"rake:deploy",
|
||||
"rake:db:migrate",
|
||||
"rake:db:schema:load",
|
||||
"rake:test",
|
||||
"rake:default",
|
||||
"rspec"
|
||||
]
|
||||
);
|
||||
assert_eq!(cmdline(find(&c, "rails:server")), "ruby bin/rails server");
|
||||
assert_eq!(cmdline(find(&c, "rake:db:migrate")), "bundle exec rake db:migrate");
|
||||
assert_eq!(find(&c, "rake:db:migrate").kind, "rake");
|
||||
assert!(find(&c, "rake:test").is_test);
|
||||
assert_eq!(cmdline(find(&c, "rspec")), "bundle exec rspec");
|
||||
assert!(find(&c, "rspec").is_test);
|
||||
assert_eq!(find(&c, "rake:build").extras.folder.as_deref(), Some("Rake"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_without_bundler_runs_bare_tools() {
|
||||
let dir = project(&[("Rakefile", "task :test\n")]);
|
||||
let c = detect(dir.path());
|
||||
assert_eq!(ids(&c), ["rake:test"]);
|
||||
assert_eq!(cmdline(&c[0]), "rake test");
|
||||
assert!(detect(project(&[("a.rb", "puts 1\n")]).path()).is_empty());
|
||||
}
|
||||
|
||||
@@ -36,6 +36,12 @@ pub fn python_interpreter(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||||
.or_else(|| pinned::pinned_python(root))
|
||||
}
|
||||
|
||||
/// The project Ruby: the configured one, else the `.ruby-version` / mise / asdf
|
||||
/// install, else `ruby` on PATH.
|
||||
pub fn ruby_interpreter(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||||
setting_path(root, &s.languages.ruby.interpreter).or_else(|| crate::ruby::project_ruby(root))
|
||||
}
|
||||
|
||||
/// The configured `node` executable's directory, else the `bin` of the Node.js the
|
||||
/// project pins for mise / asdf.
|
||||
pub(crate) fn node_dir(root: &Path, s: &Settings) -> Option<PathBuf> {
|
||||
@@ -624,6 +630,7 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
|
||||
let jdk = project_jdk(root, s);
|
||||
let mvn = maven_command(root, s);
|
||||
let mvn_options = maven_options(root, s);
|
||||
let ruby = ruby_interpreter(root, s);
|
||||
|
||||
for c in configs.iter_mut() {
|
||||
resolve_run_command(c, root, s);
|
||||
@@ -709,6 +716,26 @@ pub fn apply_to_run_configs(configs: &mut [RunConfig], root: &Path, s: &Settings
|
||||
c.env.entry(key).or_insert(value);
|
||||
}
|
||||
}
|
||||
"ruby" | "rake" | "rspec" => {
|
||||
if let Some(ruby) = &ruby
|
||||
&& let Some(bin) = crate::ruby::bin_dir_of(ruby)
|
||||
{
|
||||
// `ruby`, `bundle`, `rake`, `rspec` of the chosen interpreter when installed
|
||||
// alongside it; the rest (a project `bin/rspec` binstub) stays as is.
|
||||
let name = Path::new(&c.command).file_name().map(|n| n.to_string_lossy().into_owned());
|
||||
if let Some(name) = name
|
||||
&& matches!(name.as_str(), "ruby" | "bundle" | "rake" | "rspec" | "gem" | "irb")
|
||||
&& !c.command.contains('/')
|
||||
&& !c.command.contains('\\')
|
||||
{
|
||||
let exe = bin.join(exe(&name));
|
||||
if exe.is_file() {
|
||||
c.command = exe.to_string_lossy().into_owned();
|
||||
}
|
||||
}
|
||||
path_dirs.push(bin);
|
||||
}
|
||||
}
|
||||
"maven" | "tomcat" => {
|
||||
if let Some(jdk) = &jdk {
|
||||
c.env.entry("JAVA_HOME".into()).or_insert_with(|| jdk.home.to_string_lossy().into_owned());
|
||||
|
||||
@@ -21,6 +21,7 @@ pub fn detect(kind: SdkKind, root: Option<&Path>) -> Vec<Sdk> {
|
||||
SdkKind::TypeScript => typescript(root),
|
||||
SdkKind::Jdk => jdk(),
|
||||
SdkKind::Maven => maven(),
|
||||
SdkKind::Ruby => crate::ruby::detect(root),
|
||||
};
|
||||
if let Some(root) = root {
|
||||
pinned_first(&mut out, kind, root);
|
||||
@@ -35,6 +36,7 @@ fn tool_of(kind: SdkKind) -> Option<Tool> {
|
||||
SdkKind::Go => Some(Tool::Go),
|
||||
SdkKind::Jdk => Some(Tool::Java),
|
||||
SdkKind::Maven => Some(Tool::Maven),
|
||||
SdkKind::Ruby => Some(Tool::Ruby),
|
||||
SdkKind::PackageManager | SdkKind::Rust | SdkKind::TypeScript => None,
|
||||
}
|
||||
}
|
||||
@@ -50,7 +52,7 @@ fn pinned_first(sdks: &mut Vec<Sdk>, kind: SdkKind, root: &Path) {
|
||||
}
|
||||
|
||||
/// Runs `f` over `items` on scoped threads (version queries spawn processes).
|
||||
fn par_map<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> Option<R> + Sync) -> Vec<R> {
|
||||
pub(crate) fn par_map<T: Sync, R: Send>(items: &[T], f: impl Fn(&T) -> Option<R> + Sync) -> Vec<R> {
|
||||
std::thread::scope(|s| {
|
||||
let handles: Vec<_> = items.iter().map(|item| s.spawn(|| f(item))).collect();
|
||||
handles.into_iter().filter_map(|h| h.join().ok().flatten()).collect()
|
||||
@@ -227,7 +229,7 @@ fn pyvenv_version(python: &Path) -> Option<String> {
|
||||
})
|
||||
}
|
||||
|
||||
fn subdirs(dir: &Path) -> Vec<PathBuf> {
|
||||
pub(crate) fn subdirs(dir: &Path) -> Vec<PathBuf> {
|
||||
let mut out: Vec<PathBuf> =
|
||||
std::fs::read_dir(dir).into_iter().flatten().flatten().map(|e| e.path()).filter(|p| p.is_dir()).collect();
|
||||
out.sort();
|
||||
|
||||
@@ -19,6 +19,7 @@ pub mod lombok;
|
||||
mod npm;
|
||||
pub mod pinned;
|
||||
pub mod python;
|
||||
pub mod ruby;
|
||||
mod vue;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -42,6 +43,7 @@ pub enum SdkKind {
|
||||
Jdk,
|
||||
/// Maven homes.
|
||||
Maven,
|
||||
Ruby,
|
||||
}
|
||||
|
||||
/// One installed interpreter / toolchain.
|
||||
|
||||
@@ -54,6 +54,7 @@ const JAVA_MANIFESTS: &[&str] =
|
||||
const NODE_MANIFESTS: &[&str] = &["package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb"];
|
||||
const CARGO_MANIFESTS: &[&str] = &["Cargo.toml", "Cargo.lock", "rust-toolchain", "rust-toolchain.toml"];
|
||||
const GO_MANIFESTS: &[&str] = &["go.mod", "go.sum", "go.work"];
|
||||
const RUBY_MANIFESTS: &[&str] = &["Gemfile", "Gemfile.lock", ".ruby-version", ".tool-versions", "mise.toml"];
|
||||
|
||||
/// A Maven / Gradle dependency of the project, from the build model.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
@@ -70,7 +71,7 @@ pub struct BuildDependency {
|
||||
/// `dependencies` (from the build model) are listed when their jars are in the local repositories.
|
||||
pub fn project_libraries(root: &Path, settings: &Settings, dependencies: &[BuildDependency]) -> Vec<LibraryGroup> {
|
||||
type Detect = fn(&Path, &Settings) -> Vec<LibraryGroup>;
|
||||
let detectors: [Detect; 5] = [python_groups, jdk_groups, node_groups, rust_groups, go_groups];
|
||||
let detectors: [Detect; 6] = [python_groups, jdk_groups, node_groups, rust_groups, go_groups, ruby_groups];
|
||||
std::thread::scope(|s| {
|
||||
let handles: Vec<_> = detectors.iter().map(|d| s.spawn(move || d(root, settings))).collect();
|
||||
let deps = s.spawn(|| dependency_groups(root, settings, dependencies));
|
||||
@@ -193,6 +194,24 @@ pub(crate) fn named_roots(roots: Vec<(String, PathBuf)>) -> Vec<LibraryRoot> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ ruby
|
||||
|
||||
fn ruby_groups(root: &Path, s: &Settings) -> Vec<LibraryGroup> {
|
||||
let Some(files) = manifests(root, RUBY_MANIFESTS) else { return Vec::new() };
|
||||
if !root.join("Gemfile").is_file() && !root.join(".ruby-version").is_file() {
|
||||
return Vec::new();
|
||||
}
|
||||
let Some(ruby) = crate::apply::ruby_interpreter(root, s) else { return Vec::new() };
|
||||
cached(root, "ruby", ruby.display().to_string(), &files, || {
|
||||
let name = crate::ruby::ruby_sdk(&ruby, "system").map(|s| s.name).unwrap_or_else(|| "Ruby".into());
|
||||
let (stdlib, gems) = crate::ruby::library_roots(&ruby, Some(root));
|
||||
let mut out = Vec::new();
|
||||
out.extend(group(name, "ruby", named_roots(stdlib)));
|
||||
out.extend(group("Gems", "gems", named_roots(gems)));
|
||||
out
|
||||
})
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ jdk
|
||||
|
||||
fn jdk_groups(root: &Path, s: &Settings) -> Vec<LibraryGroup> {
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
//! Ruby interpreters: the ones a version manager installed (rbenv, rvm, chruby,
|
||||
//! mise, asdf), Homebrew's, and the ones on `PATH`; the project's `.ruby-version`;
|
||||
//! and the interpreter's standard library and Bundler gems for External Libraries.
|
||||
|
||||
use crate::discover::{par_map, subdirs};
|
||||
use crate::exec::{executables_in, query, query_in, search_path};
|
||||
use crate::{Sdk, exe, home_dir, pinned};
|
||||
use ide_core::version_managers::{self, Tool};
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
const VERSION_TIMEOUT: Duration = Duration::from_secs(4);
|
||||
const QUERY_TIMEOUT: Duration = Duration::from_secs(20);
|
||||
|
||||
fn var_path(key: &str) -> Option<PathBuf> {
|
||||
ide_core::process::host_var_os(key).filter(|v| !v.is_empty()).map(PathBuf::from)
|
||||
}
|
||||
|
||||
/// Where version managers keep one directory per Ruby, with the manager's name.
|
||||
fn manager_roots() -> Vec<(PathBuf, &'static str)> {
|
||||
let mut out = Vec::new();
|
||||
let Some(home) = home_dir() else { return out };
|
||||
out.push((var_path("RBENV_ROOT").unwrap_or_else(|| home.join(".rbenv")).join("versions"), "rbenv"));
|
||||
out.push((var_path("rvm_path").unwrap_or_else(|| home.join(".rvm")).join("rubies"), "rvm"));
|
||||
out.push((PathBuf::from("/usr/local/rvm/rubies"), "rvm"));
|
||||
out.push((home.join(".rubies"), "chruby"));
|
||||
out.push((PathBuf::from("/opt/rubies"), "chruby"));
|
||||
out
|
||||
}
|
||||
|
||||
/// `bin/ruby` of a Ruby install directory.
|
||||
pub(crate) fn ruby_in_install(dir: &Path) -> Option<PathBuf> {
|
||||
let p = dir.join("bin").join(exe("ruby"));
|
||||
p.is_file().then_some(p)
|
||||
}
|
||||
|
||||
/// Installed Rubies: version-manager installs, Homebrew, then `PATH` (shims excluded).
|
||||
pub(crate) fn detect(root: Option<&Path>) -> Vec<Sdk> {
|
||||
let mut found: Vec<(PathBuf, &'static str)> = Vec::new();
|
||||
for (dir, manager) in manager_roots() {
|
||||
found.extend(subdirs(&dir).into_iter().filter_map(|v| ruby_in_install(&v)).map(|p| (p, manager)));
|
||||
}
|
||||
for (dir, manager) in version_managers::installs(Tool::Ruby) {
|
||||
found.extend(ruby_in_install(&dir).map(|p| (p, manager)));
|
||||
}
|
||||
for brew in ["/opt/homebrew/opt/ruby", "/usr/local/opt/ruby", "/home/linuxbrew/.linuxbrew/opt/ruby"] {
|
||||
found.extend(ruby_in_install(Path::new(brew)).map(|p| (p, "homebrew")));
|
||||
}
|
||||
let dirs = std::env::split_paths(&search_path())
|
||||
.filter(|d| !is_shims_dir(d) && !version_managers::is_shims_dir(d))
|
||||
.collect::<Vec<_>>();
|
||||
found.extend(executables_in("ruby", dirs.into_iter()).into_iter().map(|p| (p, "system")));
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
let candidates: Vec<(PathBuf, &'static str)> = found
|
||||
.into_iter()
|
||||
.filter(|(p, _)| p.is_file() && seen.insert(p.canonicalize().unwrap_or_else(|_| p.clone())))
|
||||
.collect();
|
||||
let mut out = par_map(&candidates, |(path, source)| ruby_sdk(path, source));
|
||||
if let Some(root) = root
|
||||
&& let Some(pinned) = pinned_ruby_version(root)
|
||||
{
|
||||
// The `.ruby-version` install first, like the project virtualenv for Python.
|
||||
if let Some(i) = out.iter().position(|s| version_matches(s.version.as_deref().unwrap_or(""), &pinned)) {
|
||||
let sdk = out.remove(i);
|
||||
out.insert(0, sdk);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// rbenv's and rvm's shim directories, which only forward to an install.
|
||||
fn is_shims_dir(dir: &Path) -> bool {
|
||||
dir.ends_with(".rbenv/shims") || dir.ends_with(".rvm/bin")
|
||||
}
|
||||
|
||||
/// `ruby 3.3.1 (2024-04-23 revision …) [arm64-darwin23]` → `3.3.1`.
|
||||
pub(crate) fn parse_version(out: &str) -> Option<String> {
|
||||
out.strip_prefix("ruby ")?
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.map(|v| v.trim_end_matches(|c: char| c.is_ascii_alphabetic() || c == '-').to_owned())
|
||||
}
|
||||
|
||||
pub(crate) fn ruby_sdk(path: &Path, source: &str) -> Option<Sdk> {
|
||||
let version = query(path, &["--version"], VERSION_TIMEOUT).and_then(|v| parse_version(&v))?;
|
||||
let short = version.split('.').take(2).collect::<Vec<_>>().join(".");
|
||||
let name = match source {
|
||||
"system" => format!("Ruby {short}"),
|
||||
tool => format!("Ruby {short} ({tool})"),
|
||||
};
|
||||
Some(Sdk {
|
||||
path: path.into(),
|
||||
name,
|
||||
version: Some(version),
|
||||
source: source.to_owned(),
|
||||
home: path.parent().and_then(Path::parent).map(Path::to_path_buf).map(Into::into),
|
||||
extra: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The version `.ruby-version` pins (rbenv / rvm / chruby read it; `ruby-3.3.1` → `3.3.1`),
|
||||
/// from `root` up, else the mise / asdf pin.
|
||||
pub fn pinned_ruby_version(root: &Path) -> Option<String> {
|
||||
let from_file = root.ancestors().find_map(|dir| {
|
||||
let text = std::fs::read_to_string(dir.join(".ruby-version")).ok()?;
|
||||
let v = text.lines().map(str::trim).find(|l| !l.is_empty() && !l.starts_with('#'))?;
|
||||
Some(v.strip_prefix("ruby-").unwrap_or(v).to_owned())
|
||||
});
|
||||
from_file.or_else(|| pinned::pinned_version(root, Tool::Ruby))
|
||||
}
|
||||
|
||||
/// Whether `version` (`3.3.1`) satisfies a pin (`3.3.1`, `3.3`, `3`).
|
||||
pub(crate) fn version_matches(version: &str, pin: &str) -> bool {
|
||||
version == pin || version.strip_prefix(pin).is_some_and(|rest| rest.starts_with('.'))
|
||||
}
|
||||
|
||||
/// The Ruby for `root` without an explicit setting: the install matching
|
||||
/// `.ruby-version` (version managers, then mise / asdf), else `ruby` on PATH.
|
||||
pub fn project_ruby(root: &Path) -> Option<PathBuf> {
|
||||
if let Some(pin) = pinned_ruby_version(root) {
|
||||
let install = manager_roots()
|
||||
.into_iter()
|
||||
.flat_map(|(dir, _)| subdirs(&dir))
|
||||
.chain(version_managers::installs(Tool::Ruby).into_iter().map(|(d, _)| d))
|
||||
.find(|dir| {
|
||||
let name = dir.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
version_matches(name.strip_prefix("ruby-").unwrap_or(&name), &pin)
|
||||
});
|
||||
if let Some(ruby) = install.and_then(|d| ruby_in_install(&d)) {
|
||||
return Some(ruby);
|
||||
}
|
||||
}
|
||||
crate::exec::which("ruby")
|
||||
}
|
||||
|
||||
/// `bin` directory of `ruby` and the gems it was installed with (`gem` and `bundle` next to it).
|
||||
pub fn bin_dir_of(ruby: &Path) -> Option<PathBuf> {
|
||||
ruby.parent().map(Path::to_path_buf)
|
||||
}
|
||||
|
||||
type NamedRoots = Vec<(String, PathBuf)>;
|
||||
|
||||
/// Standard-library / site directories (`ruby/3.3.0`) and the gems Bundler resolves
|
||||
/// for `root` (`rails-7.1.3`), each as `(name, path)`.
|
||||
pub fn library_roots(ruby: &Path, root: Option<&Path>) -> (NamedRoots, NamedRoots) {
|
||||
const PATHS: &str = "require 'rbconfig'; puts RbConfig::CONFIG['rubylibdir'], RbConfig::CONFIG['sitelibdir'], RbConfig::CONFIG['vendorlibdir']";
|
||||
let mut stdlib = Vec::new();
|
||||
let mut gems = Vec::new();
|
||||
let file_name = |p: &Path| p.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
if let Some(text) = query(ruby, &["-e", PATHS], QUERY_TIMEOUT) {
|
||||
for line in text.lines().map(str::trim).filter(|l| !l.is_empty()) {
|
||||
let path = PathBuf::from(line);
|
||||
if path.is_dir() && !stdlib.iter().any(|(_, p)| *p == path) {
|
||||
let parent = path.parent().map(file_name).unwrap_or_default();
|
||||
stdlib.push((format!("{parent}/{}", file_name(&path)), path));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(root) = root.filter(|r| r.join("Gemfile").is_file()) {
|
||||
let bin = bin_dir_of(ruby);
|
||||
let bundle = bin.as_ref().map(|b| b.join(exe("bundle"))).filter(|b| b.is_file());
|
||||
let path = bin.as_ref().map(|b| crate::exec::path_with(&[b.as_path()])).unwrap_or_default();
|
||||
let env: &[(&str, &str)] = &[("PATH", &path)];
|
||||
let text = match &bundle {
|
||||
Some(b) => query_in(b, &["list", "--paths"], Some(root), env, QUERY_TIMEOUT),
|
||||
None => query_in(ruby, &["-S", "bundle", "list", "--paths"], Some(root), env, QUERY_TIMEOUT),
|
||||
};
|
||||
for line in text.unwrap_or_default().lines().map(str::trim).filter(|l| !l.is_empty()) {
|
||||
let path = PathBuf::from(line);
|
||||
if path.is_dir() && path != root {
|
||||
gems.push((file_name(&path), path));
|
||||
}
|
||||
}
|
||||
}
|
||||
(stdlib, gems)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn includes_bundled_gems_installed_inside_the_project() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let root = dir.path().join("project");
|
||||
let gem = root.join("vendor/bundle/ruby/4.0.0/gems/rake-13.4.0");
|
||||
let external = dir.path().join("gems/rspec-3.13.2");
|
||||
let bin = dir.path().join("bin");
|
||||
for path in [&gem, &external, &bin] {
|
||||
std::fs::create_dir_all(path).unwrap();
|
||||
}
|
||||
std::fs::write(root.join("Gemfile"), "gem 'rake'\ngem 'rspec'\n").unwrap();
|
||||
for (name, output) in [
|
||||
("ruby", String::new()),
|
||||
("bundle", format!("{}\n{}\n{}", gem.display(), external.display(), root.display())),
|
||||
] {
|
||||
let path = bin.join(name);
|
||||
std::fs::write(&path, format!("#!/bin/sh\nprintf '%s\\n' '{output}'\n")).unwrap();
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap();
|
||||
}
|
||||
|
||||
let (_, gems) = library_roots(&bin.join("ruby"), Some(&root));
|
||||
assert_eq!(gems, vec![("rake-13.4.0".into(), gem), ("rspec-3.13.2".into(), external)]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_ruby_version_output() {
|
||||
assert_eq!(
|
||||
parse_version("ruby 3.3.1 (2024-04-23 revision c56cd86388) [arm64-darwin23]").as_deref(),
|
||||
Some("3.3.1")
|
||||
);
|
||||
assert_eq!(parse_version("ruby 3.4.0dev (2024-10-01) +PRISM [x86_64-linux]").as_deref(), Some("3.4.0"));
|
||||
assert_eq!(parse_version("jruby 9.4.5.0"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_pins_match_prefixes() {
|
||||
assert!(version_matches("3.3.1", "3.3.1"));
|
||||
assert!(version_matches("3.3.1", "3.3"));
|
||||
assert!(!version_matches("3.31.1", "3.3"));
|
||||
assert!(!version_matches("3.3.1", "3.3.10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruby_version_file_wins_over_managers() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let project = dir.path().join("app/sub");
|
||||
std::fs::create_dir_all(&project).unwrap();
|
||||
std::fs::write(dir.path().join("app/.ruby-version"), "ruby-3.2.2\n").unwrap();
|
||||
assert_eq!(pinned_ruby_version(&project).as_deref(), Some("3.2.2"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
id = "erb"
|
||||
name = "ERB"
|
||||
extensions = ["erb", "html.erb", "rhtml", "js.erb", "text.erb", "xml.erb", "json.erb"]
|
||||
block-comment = ["<%#", "%>"]
|
||||
indent = { tab-size = 2, insert-spaces = true }
|
||||
ui-mode = "ERB"
|
||||
+7
-1
@@ -4098,6 +4098,7 @@ export type LanguagesSettings = {
|
||||
typescript?: TypeScriptSettings,
|
||||
prettier?: PrettierSettings,
|
||||
java?: JavaSettings,
|
||||
ruby?: RubySettings,
|
||||
};
|
||||
|
||||
/** What a freshly opened window should do once its frontend is up. */
|
||||
@@ -5325,6 +5326,11 @@ export type RevisionChange = {
|
||||
|
||||
export type RootKind = "source" | "test" | "resources" | "testResources" | "excluded";
|
||||
|
||||
export type RubySettings = {
|
||||
/** Project interpreter executable; empty = auto-detect (`.ruby-version`, mise / asdf, else `ruby` on PATH). */
|
||||
interpreter?: string,
|
||||
};
|
||||
|
||||
export type RouteSource = {
|
||||
path: string,
|
||||
text: string,
|
||||
@@ -5538,7 +5544,7 @@ export type SdkKind = "python" | "node" |
|
||||
/** JDK homes. */
|
||||
"jdk" |
|
||||
/** Maven homes. */
|
||||
"maven";
|
||||
"maven" | "ruby";
|
||||
|
||||
/**
|
||||
* IDEA's search context: matches anywhere, only in, or outside comments and
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
|
||||
import { classHighlighter, highlightTree } from "@lezer/highlight";
|
||||
import { findLanguageDescription, loadSupport } from "./languages";
|
||||
|
||||
async function tokens(source: string) {
|
||||
const desc = findLanguageDescription("ERB", "show.html.erb")!;
|
||||
const support = await loadSupport(desc);
|
||||
const state = EditorState.create({ doc: source, extensions: [support!] });
|
||||
ensureSyntaxTree(state, source.length, 1000);
|
||||
const result: { text: string; style: string }[] = [];
|
||||
highlightTree(syntaxTree(state), classHighlighter, (from, to, style) =>
|
||||
result.push({ text: source.slice(from, to), style }),
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
describe("ERB mode", () => {
|
||||
it("highlights HTML outside and Ruby inside the tags", async () => {
|
||||
const out = await tokens('<div class="x"><%= user.name if user %></div>\n<%# note %>\n<% 42 %>');
|
||||
expect(out).toContainEqual(expect.objectContaining({ text: "div", style: "tok-typeName" }));
|
||||
expect(out).toContainEqual(expect.objectContaining({ text: "if", style: "tok-keyword" }));
|
||||
expect(out).toContainEqual(expect.objectContaining({ text: "42", style: "tok-number" }));
|
||||
expect(out).toContainEqual(expect.objectContaining({ text: "<%# note %>", style: "tok-comment" }));
|
||||
expect(out.filter((t) => t.text === "<%=" || t.text === "%>")).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("keeps Ruby state across lines", async () => {
|
||||
const out = await tokens('<% s = "a\n<p>" %><p>');
|
||||
expect(out.filter((t) => t.text === "p")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* ERB (Embedded Ruby) editor mode: HTML outside `<% … %>`, Ruby inside. Each
|
||||
* region is tokenized by its own legacy stream mode on a sub-stream bounded at
|
||||
* the next delimiter, so neither mode sees the other's text.
|
||||
*/
|
||||
import { LanguageSupport, StreamLanguage, StringStream, type StreamParser } from "@codemirror/language";
|
||||
import { tags } from "@lezer/highlight";
|
||||
|
||||
type State<H, R> = { html: H; ruby: R; inRuby: boolean; indentUnit: number };
|
||||
|
||||
const OPEN = /<%(?:==|=|-|#)?/;
|
||||
const CLOSE = /-?%>/;
|
||||
|
||||
export function erbParser<H, R>(html: StreamParser<H>, ruby: StreamParser<R>): StreamParser<State<H, R>> {
|
||||
const copy = <S>(base: StreamParser<S>, state: S): S =>
|
||||
base.copyState
|
||||
? base.copyState(state)
|
||||
: (Object.fromEntries(
|
||||
Object.entries(state as object).map(([k, v]) => [k, Array.isArray(v) ? v.slice() : v]),
|
||||
) as S);
|
||||
|
||||
/** Runs `base` on `stream` cut at `end`, then advances `stream` to where it stopped. */
|
||||
const within = <S>(stream: StringStream, end: number, base: StreamParser<S>, state: S): string | null => {
|
||||
const sub = new StringStream(stream.string.slice(0, end), 4, stream.indentUnit);
|
||||
sub.pos = stream.pos;
|
||||
sub.start = stream.start;
|
||||
const style = base.token(sub, state);
|
||||
stream.pos = Math.max(sub.pos, stream.pos + (sub.pos === stream.pos ? 1 : 0));
|
||||
return style;
|
||||
};
|
||||
|
||||
return {
|
||||
name: "erb",
|
||||
languageData: { ...html.languageData, commentTokens: { block: { open: "<%#", close: "%>" } } },
|
||||
tokenTable: { ...html.tokenTable, ...ruby.tokenTable, erbDelimiter: tags.special(tags.brace) },
|
||||
startState: (indentUnit) => ({
|
||||
html: html.startState!(indentUnit),
|
||||
ruby: ruby.startState!(indentUnit),
|
||||
inRuby: false,
|
||||
indentUnit,
|
||||
}),
|
||||
copyState: (s) => ({ ...s, html: copy(html, s.html), ruby: copy(ruby, s.ruby) }),
|
||||
blankLine: (s, unit) => (s.inRuby ? ruby.blankLine?.(s.ruby, unit) : html.blankLine?.(s.html, unit)),
|
||||
indent: (s, text, cx) => (s.inRuby ? ruby.indent?.(s.ruby, text, cx) : html.indent?.(s.html, text, cx)) ?? null,
|
||||
token(stream, state) {
|
||||
const rest = stream.string.slice(stream.pos);
|
||||
if (!state.inRuby) {
|
||||
const open = OPEN.exec(rest);
|
||||
if (open?.index === 0) {
|
||||
stream.pos += open[0].length;
|
||||
if (open[0] === "<%#") {
|
||||
const close = CLOSE.exec(stream.string.slice(stream.pos));
|
||||
stream.pos = close ? stream.pos + close.index + close[0].length : stream.string.length;
|
||||
return "comment";
|
||||
}
|
||||
state.inRuby = true;
|
||||
return "erbDelimiter";
|
||||
}
|
||||
return within(stream, open ? stream.pos + open.index : stream.string.length, html, state.html);
|
||||
}
|
||||
const close = CLOSE.exec(rest);
|
||||
if (close?.index === 0) {
|
||||
stream.pos += close[0].length;
|
||||
state.inRuby = false;
|
||||
return "erbDelimiter";
|
||||
}
|
||||
return within(stream, close ? stream.pos + close.index : stream.string.length, ruby, state.ruby);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function erb(): Promise<LanguageSupport> {
|
||||
const [{ html }, { ruby }] = await Promise.all([
|
||||
import("@codemirror/legacy-modes/mode/xml"),
|
||||
import("@codemirror/legacy-modes/mode/ruby"),
|
||||
]);
|
||||
return new LanguageSupport(StreamLanguage.define(erbParser(html, ruby)));
|
||||
}
|
||||
@@ -96,6 +96,12 @@ const OWN_MODES = [
|
||||
}),
|
||||
),
|
||||
LanguageDescription.of({ name: "Angular Template", load: () => import("./angular").then((m) => m.angular()) }),
|
||||
LanguageDescription.of({
|
||||
name: "ERB",
|
||||
alias: ["erb", "rhtml"],
|
||||
extensions: ["erb", "rhtml"],
|
||||
load: () => import("./erb").then((m) => m.erb()),
|
||||
}),
|
||||
];
|
||||
|
||||
const BATCH_KEYWORDS = new Set([
|
||||
|
||||
@@ -26,6 +26,8 @@ const GROUP_ICONS: Record<string, string> = {
|
||||
goModules: "file-gomod",
|
||||
maven: "maven-project",
|
||||
gradle: "file-gradle",
|
||||
ruby: "file-ruby",
|
||||
gems: "file-ruby",
|
||||
};
|
||||
|
||||
/** An archive's root (`/jdk/lib/src.zip!`): its entries live below `<archive>!/`. */
|
||||
|
||||
@@ -152,6 +152,8 @@ describe("context configuration", () => {
|
||||
cfg({ id: "pytest", kind: "pytest", command: "uv", args: ["run", "pytest"], isTest: true }),
|
||||
cfg({ id: "python:module:app", kind: "python", command: ".venv/bin/python", args: ["-m", "app"] }),
|
||||
cfg({ id: "gradle:test", kind: "gradle", command: "./gradlew", args: ["test"], isTest: true }),
|
||||
cfg({ id: "rspec", kind: "rspec", command: "bundle", args: ["exec", "rspec"], isTest: true }),
|
||||
cfg({ id: "rake:test", kind: "rake", command: "bundle", args: ["exec", "rake", "test"], isTest: true }),
|
||||
];
|
||||
|
||||
it("classifies files", () => {
|
||||
@@ -163,6 +165,9 @@ describe("context configuration", () => {
|
||||
expect(fileRole("polls/tests.py")).toBe("script");
|
||||
expect(fileRole("src/test/kotlin/OrderControllerTest.kt")).toBe("test");
|
||||
expect(fileRole("tests/integration.rs")).toBe("test");
|
||||
expect(fileRole("spec/models/user_spec.rb")).toBe("test");
|
||||
expect(fileRole("test/user_test.rb")).toBe("test");
|
||||
expect(fileRole("lib/tasks/gen.rb")).toBe("script");
|
||||
expect(fileRole("src/main.rs")).toBe("main");
|
||||
expect(fileRole("cmd/main.go", "package main\nfunc main() {}")).toBe("main");
|
||||
expect(fileRole("tools/gen.py")).toBe("script");
|
||||
@@ -184,6 +189,20 @@ describe("context configuration", () => {
|
||||
expect(contextConfig("src/a.spec.js", configs)!.args).toEqual(["run", "test", "--", "src/a.spec.js"]);
|
||||
});
|
||||
|
||||
it("narrows rspec and rake test runs to the file", () => {
|
||||
expect(contextConfig("spec/models/user_spec.rb", configs)!.args).toEqual([
|
||||
"exec",
|
||||
"rspec",
|
||||
"spec/models/user_spec.rb",
|
||||
]);
|
||||
expect(contextConfig("test/user_test.rb", configs)!.args).toEqual([
|
||||
"exec",
|
||||
"rake",
|
||||
"test",
|
||||
"TEST=test/user_test.rb",
|
||||
]);
|
||||
});
|
||||
|
||||
it("narrows go, cargo, pytest and gradle runs", () => {
|
||||
expect(contextConfig("internal/calc/calc_test.go", configs)!.args).toEqual(["test", "./internal/calc"]);
|
||||
expect(contextConfig("tests/api.rs", configs)!.args).toEqual(["test", "--test", "api"]);
|
||||
|
||||
@@ -19,6 +19,7 @@ export function fileRole(relPath: string, content?: string, windows = isWindows(
|
||||
/^test_.*\.py$|_test\.py$/.test(name) ||
|
||||
(django && name === "tests.py") ||
|
||||
/_test\.go$/.test(name) ||
|
||||
/_(spec|test)\.rb$/.test(name) ||
|
||||
/(Test|Tests|Spec|IT)\.(kt|java|scala|groovy)$/.test(name) ||
|
||||
(/(^|\/)tests\/[^/]+\.rs$/.test(relPath) && name !== "mod.rs")
|
||||
)
|
||||
@@ -27,7 +28,7 @@ export function fileRole(relPath: string, content?: string, windows = isWindows(
|
||||
if (/\.go$/.test(name) && content !== undefined && /^\s*package\s+main\b/m.test(content)) return "main";
|
||||
if (/\.(kt|java)$/.test(name) && content !== undefined && /\bfun\s+main\s*\(|static\s+void\s+main\s*\(/.test(content))
|
||||
return "main";
|
||||
if (/\.(py|sh|bash|zsh|[cm]?js|ts)$/.test(name)) return "script";
|
||||
if (/\.(py|rb|sh|bash|zsh|[cm]?js|ts)$/.test(name)) return "script";
|
||||
if (windows && /\.(bat|cmd)$/i.test(name)) return "script";
|
||||
return null;
|
||||
}
|
||||
@@ -39,6 +40,8 @@ const stem = (p: string) => (p.split("/").pop() ?? p).replace(/\.[^.]+$/, "");
|
||||
function testKinds(relPath: string): string[] {
|
||||
if (/\.[cm]?[jt]sx?$/.test(relPath)) return ["npm", "deno", "bun"];
|
||||
if (/\.py$/.test(relPath)) return ["pytest", "python", "django-test"];
|
||||
if (/_spec\.rb$/.test(relPath)) return ["rspec", "rake"];
|
||||
if (/_test\.rb$/.test(relPath)) return ["rake", "rspec"];
|
||||
if (/\.go$/.test(relPath)) return ["go"];
|
||||
if (/\.rs$/.test(relPath)) return ["cargo"];
|
||||
if (/\.(kt|java|scala|groovy)$/.test(relPath)) return ["gradle", "maven"];
|
||||
@@ -136,7 +139,9 @@ function implicitPytest(relPath: string, configs: RunConfig[]): RunConfig | null
|
||||
export function testBasesFor(relPath: string, configs: RunConfig[]): RunConfig[] {
|
||||
const kinds = testKinds(relPath);
|
||||
// Configurations produced from a test or file are results of narrowing, never bases for it.
|
||||
const candidates = configs.filter((c) => c.isTest && kinds.includes(c.kind) && !c.id.startsWith("context:"));
|
||||
const candidates = configs
|
||||
.filter((c) => c.isTest && kinds.includes(c.kind) && !c.id.startsWith("context:"))
|
||||
.sort((a, b) => kinds.indexOf(a.kind) - kinds.indexOf(b.kind));
|
||||
const best = allClosest(candidates, relPath);
|
||||
if (best.length) return best;
|
||||
const fallback = candidates[0] ?? implicitPytest(relPath, configs);
|
||||
@@ -162,7 +167,14 @@ export function testConfigFor(relPath: string, configs: RunConfig[], base?: RunC
|
||||
return derived(base, relPath, { args: [...base.args, file] });
|
||||
case "pytest":
|
||||
case "python":
|
||||
case "rspec":
|
||||
return derived(base, relPath, { args: [...base.args, file] });
|
||||
case "rake": {
|
||||
// `rake test` (minitest) takes the file as TEST=; `rake spec` as SPEC=.
|
||||
const task = base.args.at(-1) ?? "";
|
||||
const key = /(^|:)spec$/.test(task) ? "SPEC" : /(^|:)test$/.test(task) ? "TEST" : null;
|
||||
return key ? derived(base, relPath, { args: [...base.args, `${key}=${file}`] }) : base;
|
||||
}
|
||||
case "django-test": {
|
||||
const tests = djangoTestsOf(base, [djangoLabel(file)]);
|
||||
return tests ? derived(base, relPath, tests) : null;
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Ruby run configuration types: a script (`ruby [options] script [args]`), a Rake
|
||||
* task and RSpec, the latter two through `bundle exec` when the project uses Bundler.
|
||||
* The interpreter itself comes from Settings › Ruby (`apply_to_run_configs`).
|
||||
*/
|
||||
import type { RunConfig } from "@/platform/ipc";
|
||||
import type { RunConfigTypeContribution } from "@/workbench/contributions";
|
||||
import { formatArgs, parseArgs } from "./configs";
|
||||
import { commandName } from "./commandName";
|
||||
|
||||
const on = (v: string | undefined) => v === "true";
|
||||
|
||||
/** `bundle exec <tool>` or `<tool>`, launched through `params.command` when one is set. */
|
||||
function tool(p: Readonly<Record<string, string>>, name: string): { command: string; args: string[] } {
|
||||
const command = p.command?.trim();
|
||||
return on(p.bundler)
|
||||
? { command: command || "bundle", args: ["exec", name] }
|
||||
: { command: command || name, args: [] };
|
||||
}
|
||||
|
||||
/** Splits `bundle exec <tool> rest…` / `<tool> rest…` into the launcher, the Bundler flag and `rest`. */
|
||||
function unwrap(c: RunConfig, name: string): { command: string; bundler: string; rest: string[] } | null {
|
||||
const cmd = commandName(c.command);
|
||||
const bare = c.command === cmd;
|
||||
if (cmd === "bundle" && c.args[0] === "exec" && c.args[1] === name)
|
||||
return { command: bare ? "" : c.command, bundler: "true", rest: c.args.slice(2) };
|
||||
if (cmd === name) return { command: bare ? "" : c.command, bundler: "", rest: c.args };
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Index of the first operand in `args` (the script, spec target or Rake task): the
|
||||
* first argument that is not an option, nor the value of the option before it. A
|
||||
* token after an option without `=` is that option's value when the option is known
|
||||
* to take one (`-I lib`, `--format documentation`) or, with `operand`, when the token
|
||||
* does not look like an operand (`--foo bar spec/a_spec.rb`); `--` ends the options.
|
||||
*/
|
||||
function operandIndex(
|
||||
args: readonly string[],
|
||||
valueOptions: ReadonlySet<string>,
|
||||
operand: (a: string) => boolean = () => true,
|
||||
): number {
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i]!;
|
||||
if (a === "--") return i + 1 < args.length ? i + 1 : -1;
|
||||
if (a.startsWith("-")) continue;
|
||||
const prev = i > 0 ? args[i - 1]! : "";
|
||||
const value = prev.startsWith("-") && !prev.includes("=") && (valueOptions.has(prev) || !operand(a));
|
||||
if (!value) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const RUBY_VALUE_OPTIONS = new Set(
|
||||
"-I -r -C -E -e -F --encoding --external-encoding --internal-encoding --enable --disable --dump".split(" "),
|
||||
);
|
||||
const RSPEC_VALUE_OPTIONS = new Set(
|
||||
(
|
||||
"-I -r --require -O --options -P --pattern --exclude-pattern --default-path -o --out --deprecation-out " +
|
||||
"-f --format -e --example -E --example-matches -t --tag --seed --order"
|
||||
).split(" "),
|
||||
);
|
||||
const RAKE_VALUE_OPTIONS = new Set(
|
||||
(
|
||||
"-f --rakefile -I --libdir -r --require -R --rakelibdir -j --jobs -C --directory " +
|
||||
"-e --execute -E --execute-continue -p --execute-print"
|
||||
).split(" "),
|
||||
);
|
||||
|
||||
const looksLikeScript = (a: string) => /[\\/]|\.rb$/.test(a);
|
||||
const looksLikeSpecTarget = (a: string) => /[\\/]|\.rb(:\d+|\[[\d:]+\])?$|^(spec|\.)$/.test(a);
|
||||
|
||||
const bundlerField = {
|
||||
key: "bundler",
|
||||
label: "Run with Bundler (bundle exec)",
|
||||
control: "checkbox",
|
||||
} as const;
|
||||
|
||||
const commandField = (name: string) =>
|
||||
({
|
||||
key: "command",
|
||||
label: "Launcher",
|
||||
mono: true,
|
||||
placeholder: name,
|
||||
hint: "Path of the tool (bundle when run with Bundler); empty uses the interpreter from Settings › Ruby",
|
||||
}) as const;
|
||||
|
||||
export const rubyScriptType: RunConfigTypeContribution = {
|
||||
id: "ruby",
|
||||
label: "Ruby",
|
||||
icon: "file-ruby",
|
||||
description: "Run a Ruby script",
|
||||
defaults: {},
|
||||
fields: [
|
||||
{ key: "script", label: "Ruby script", mono: true, required: true, placeholder: "e.g. bin/rails or lib/app.rb" },
|
||||
{ key: "scriptArgs", label: "Script arguments", mono: true, placeholder: "e.g. server -p 3000" },
|
||||
{ key: "rubyArgs", label: "Ruby arguments", mono: true, placeholder: "e.g. -Ilib -w" },
|
||||
bundlerField,
|
||||
commandField("ruby"),
|
||||
],
|
||||
commandLine(p) {
|
||||
const t = tool(p, "ruby");
|
||||
return {
|
||||
command: t.command,
|
||||
args: [
|
||||
...t.args,
|
||||
...parseArgs(p.rubyArgs ?? ""),
|
||||
...parseArgs(p.script?.trim() ?? ""),
|
||||
...parseArgs(p.scriptArgs ?? ""),
|
||||
],
|
||||
};
|
||||
},
|
||||
paramsFrom(c) {
|
||||
if (c.kind !== "ruby") return null;
|
||||
const u = unwrap(c, "ruby");
|
||||
if (!u) return null;
|
||||
const at = operandIndex(u.rest, RUBY_VALUE_OPTIONS, looksLikeScript);
|
||||
// A new configuration (`ruby`, nothing else) opens in the form with the script still to fill in;
|
||||
// options without a script (`ruby -e …`, `ruby -v`) stay a plain command.
|
||||
if (at < 0)
|
||||
return u.rest.length
|
||||
? null
|
||||
: { command: u.command, bundler: u.bundler, rubyArgs: "", script: "", scriptArgs: "" };
|
||||
return {
|
||||
command: u.command,
|
||||
bundler: u.bundler,
|
||||
rubyArgs: formatArgs(u.rest.slice(0, at)),
|
||||
script: formatArgs([u.rest[at]!]),
|
||||
scriptArgs: formatArgs(u.rest.slice(at + 1)),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export const rakeType: RunConfigTypeContribution = {
|
||||
id: "rake",
|
||||
label: "Rake",
|
||||
icon: "file-ruby",
|
||||
description: "Run a Rake task",
|
||||
defaults: { bundler: "true" },
|
||||
fields: [
|
||||
{
|
||||
key: "task",
|
||||
label: "Task",
|
||||
mono: true,
|
||||
placeholder: "default",
|
||||
hint: "Task name with arguments, e.g. db:migrate or test TEST=test/a_test.rb; empty runs the default task",
|
||||
suggestions: (configs) => [
|
||||
...new Set(configs.filter((c) => c.kind === "rake" && c.detected).map((c) => c.args.at(-1) ?? "")),
|
||||
],
|
||||
},
|
||||
{ key: "options", label: "Rake options", mono: true, placeholder: "e.g. --trace" },
|
||||
bundlerField,
|
||||
commandField("rake"),
|
||||
],
|
||||
commandLine(p) {
|
||||
const t = tool(p, "rake");
|
||||
return { command: t.command, args: [...t.args, ...parseArgs(p.options ?? ""), ...parseArgs(p.task ?? "")] };
|
||||
},
|
||||
paramsFrom(c) {
|
||||
if (c.kind !== "rake") return null;
|
||||
const u = unwrap(c, "rake");
|
||||
if (!u) return null;
|
||||
const at = operandIndex(u.rest, RAKE_VALUE_OPTIONS);
|
||||
const [options, task] = at < 0 ? [u.rest, []] : [u.rest.slice(0, at), u.rest.slice(at)];
|
||||
return { command: u.command, bundler: u.bundler, options: formatArgs(options), task: formatArgs(task) };
|
||||
},
|
||||
};
|
||||
|
||||
export const rspecType: RunConfigTypeContribution = {
|
||||
id: "rspec",
|
||||
label: "RSpec",
|
||||
icon: "file-ruby",
|
||||
description: "Run RSpec examples",
|
||||
defaults: { bundler: "true" },
|
||||
fields: [
|
||||
{
|
||||
key: "target",
|
||||
label: "Target",
|
||||
mono: true,
|
||||
placeholder: "e.g. spec",
|
||||
hint: "Directory, file or file:line; empty runs the whole suite",
|
||||
},
|
||||
{ key: "options", label: "RSpec options", mono: true, placeholder: "e.g. --format documentation --fail-fast" },
|
||||
bundlerField,
|
||||
commandField("rspec"),
|
||||
],
|
||||
commandLine(p) {
|
||||
const t = tool(p, "rspec");
|
||||
return { command: t.command, args: [...t.args, ...parseArgs(p.options ?? ""), ...parseArgs(p.target ?? "")] };
|
||||
},
|
||||
paramsFrom(c) {
|
||||
if (c.kind !== "rspec") return null;
|
||||
const u = unwrap(c, "rspec");
|
||||
if (!u) return null;
|
||||
const at = operandIndex(u.rest, RSPEC_VALUE_OPTIONS, looksLikeSpecTarget);
|
||||
const [options, target] = at < 0 ? [u.rest, []] : [u.rest.slice(0, at), u.rest.slice(at)];
|
||||
return { command: u.command, bundler: u.bundler, options: formatArgs(options), target: formatArgs(target) };
|
||||
},
|
||||
};
|
||||
@@ -103,6 +103,12 @@ describe("configuration types", () => {
|
||||
["pytest", "pytest.exe", ["tests/test_app.py", "-q"]],
|
||||
["pytest", String.raw`C:\project\.venv\Scripts\PYTEST.EXE`, ["-q"]],
|
||||
["pytest", "PYTHON.EXE", ["-m", "pytest", "tests"]],
|
||||
["ruby", String.raw`C:\Ruby33\bin\ruby.exe`, ["-Ilib", "bin/app", "--port", "3000"]],
|
||||
["rake", "bundle", ["exec", "rake", "--trace", "db:migrate"]],
|
||||
["rspec", "/home/me/.rbenv/versions/3.3.1/bin/rspec", ["--fail-fast", "spec/a_spec.rb:12"]],
|
||||
["rspec", "bundle", ["exec", "rspec", "--format", "documentation", "--fail-fast", "spec/foo_spec.rb"]],
|
||||
["ruby", "ruby", ["-I", "lib", "-w", "bin/app", "--port", "3000"]],
|
||||
["rake", "bundle", ["exec", "rake", "-f", "lib/tasks.rake", "--trace", "db:migrate"]],
|
||||
["npm", String.raw`C:\Program Files\nodejs\NPM.CMD`, ["run", "test", "--", "two words"]],
|
||||
["npm", "pnpm.COM", ["test"]],
|
||||
["application", String.raw`C:\jdk\bin\JAVA.EXE`, ["-cp", "lib/*", "a.Main"]],
|
||||
@@ -172,6 +178,75 @@ describe("configuration types", () => {
|
||||
expect(godot.paramsFrom!(cfg("x", { command: "cargo" }))).toBeNull();
|
||||
});
|
||||
|
||||
it("ruby types default to Bundler for rake and rspec and mark rspec as tests", () => {
|
||||
expect(newConfigOfType(typeFor("rake")!, [])).toMatchObject({ command: "bundle", args: ["exec", "rake"] });
|
||||
const ruby = newConfigOfType(typeFor("ruby")!, []);
|
||||
expect(ruby).toMatchObject({ command: "ruby", args: [] });
|
||||
expect(formParams(ruby)).toEqual({ command: "", bundler: "", rubyArgs: "", script: "", scriptArgs: "" });
|
||||
const rspec = newConfigOfType(typeFor("rspec")!, []);
|
||||
expect(rspec).toMatchObject({ command: "bundle", args: ["exec", "rspec"], isTest: true });
|
||||
expect(withParams(rspec, { bundler: "", target: "spec/a_spec.rb", options: "-fd" })).toMatchObject({
|
||||
command: "rspec",
|
||||
args: ["-fd", "spec/a_spec.rb"],
|
||||
isTest: true,
|
||||
});
|
||||
expect(formParams(cfg("r", { kind: "ruby", command: "ruby", args: ["app.rb", "-v"] }))).toEqual({
|
||||
command: "",
|
||||
bundler: "",
|
||||
rubyArgs: "",
|
||||
script: "app.rb",
|
||||
scriptArgs: "-v",
|
||||
});
|
||||
});
|
||||
|
||||
it("ruby types keep option values with their options and the argument order as written", () => {
|
||||
const ruby = (args: string[]) => formParams(cfg("r", { kind: "ruby", command: "ruby", args }));
|
||||
expect(ruby(["-I", "lib", "-w", "bin/app", "--port", "3000"])).toEqual({
|
||||
command: "",
|
||||
bundler: "",
|
||||
rubyArgs: "-I lib -w",
|
||||
script: "bin/app",
|
||||
scriptArgs: "--port 3000",
|
||||
});
|
||||
expect(ruby(["-W0", "--", "-weird.rb", "x"])).toMatchObject({
|
||||
rubyArgs: "-W0 --",
|
||||
script: "-weird.rb",
|
||||
scriptArgs: "x",
|
||||
});
|
||||
expect(ruby(["-e", "puts 1"])).toBeNull();
|
||||
expect(ruby(["-v"])).toBeNull();
|
||||
|
||||
const rspec = (args: string[]) =>
|
||||
formParams(cfg("s", { kind: "rspec", command: "bundle", args: ["exec", "rspec", ...args] }));
|
||||
expect(rspec(["--format", "documentation", "--fail-fast", "spec/foo_spec.rb"])).toEqual({
|
||||
command: "",
|
||||
bundler: "true",
|
||||
options: "--format documentation --fail-fast",
|
||||
target: "spec/foo_spec.rb",
|
||||
});
|
||||
expect(rspec(["--tag", "~slow", "-fd", "spec/a_spec.rb:12", "spec/b_spec.rb"])).toMatchObject({
|
||||
options: "--tag ~slow -fd",
|
||||
target: "spec/a_spec.rb:12 spec/b_spec.rb",
|
||||
});
|
||||
expect(rspec(["--format", "documentation"])).toMatchObject({ options: "--format documentation", target: "" });
|
||||
expect(rspec(["--new-option", "value", "spec"])).toMatchObject({ options: "--new-option value", target: "spec" });
|
||||
expect(rspec([])).toMatchObject({ options: "", target: "" });
|
||||
|
||||
const rake = (args: string[]) =>
|
||||
formParams(cfg("k", { kind: "rake", command: "bundle", args: ["exec", "rake", ...args] }));
|
||||
expect(rake(["-f", "lib/tasks.rake", "--trace", "db:migrate"])).toEqual({
|
||||
command: "",
|
||||
bundler: "true",
|
||||
options: "-f lib/tasks.rake --trace",
|
||||
task: "db:migrate",
|
||||
});
|
||||
expect(rake(["--trace", "test", "TEST=test/a_test.rb"])).toMatchObject({
|
||||
options: "--trace",
|
||||
task: "test TEST=test/a_test.rb",
|
||||
});
|
||||
expect(rake(["-T"])).toMatchObject({ options: "-T", task: "" });
|
||||
});
|
||||
|
||||
it("Django tests round-trip commandLine / paramsFrom and always report to the test tree", () => {
|
||||
const django = typeFor("django-test")!;
|
||||
expect(django.debugAdapter).toBe("debugpy");
|
||||
|
||||
@@ -23,6 +23,7 @@ import { tomcatType } from "./tomcat";
|
||||
import { goBuildType, goTestType } from "./goTypes";
|
||||
|
||||
import { dotnetProjectType, dotnetTestType } from "./dotnetTypes";
|
||||
import { rakeType, rspecType, rubyScriptType } from "./rubyTypes";
|
||||
import { djangoTestType } from "./djangoTypes";
|
||||
|
||||
export const COMPOUND = "compound";
|
||||
@@ -399,6 +400,9 @@ export const BUILTIN_TYPES: readonly RunConfigTypeContribution[] = [
|
||||
maven,
|
||||
godot,
|
||||
pytest,
|
||||
rubyScriptType,
|
||||
rakeType,
|
||||
rspecType,
|
||||
djangoTestType,
|
||||
dotnetProjectType,
|
||||
dotnetTestType,
|
||||
@@ -453,7 +457,7 @@ export function withParams(config: RunConfig, params: Record<string, string>, ty
|
||||
...(config.kind === "dotnet" && config.beforeLaunch === undefined
|
||||
? { beforeLaunch: [{ kind: "dotnetBuild", target: "" }] }
|
||||
: {}),
|
||||
...(config.kind === "dotnet-test" || config.kind === "django-test" ? { isTest: true } : {}),
|
||||
...(config.kind === "dotnet-test" || config.kind === "django-test" || config.kind === "rspec" ? { isTest: true } : {}),
|
||||
params: params.goKind ? { ...params, programArgv: JSON.stringify(parseArgs(params.programArgs ?? "")) } : params,
|
||||
...(config.kind === "go-test" && !config.id.startsWith("template:") ? { kind: "go" } : {}),
|
||||
...(params.goKind ? { isTest: params.goKind === "test" } : {}),
|
||||
|
||||
@@ -35,6 +35,14 @@ describe("languages settings", () => {
|
||||
expect(validateSettings({}).languages).toEqual(DEFAULT_LANGUAGES);
|
||||
});
|
||||
|
||||
it("keeps the Ruby interpreter through settings normalization and defaults to Auto", () => {
|
||||
expect(validateSettings({}).languages.ruby.interpreter).toBe("");
|
||||
expect(validateLanguages({ ruby: { interpreter: 42 } }).ruby.interpreter).toBe("");
|
||||
const settings = validateSettings({ languages: { ruby: { interpreter: " /opt/ruby/bin/ruby " } } });
|
||||
expect(settings.languages.ruby.interpreter).toBe("/opt/ruby/bin/ruby");
|
||||
expect(validateSettings(settings).languages.ruby.interpreter).toBe("/opt/ruby/bin/ruby");
|
||||
});
|
||||
|
||||
it("keeps Prettier opt-in on save and normalizes its enabled languages", () => {
|
||||
expect(validateLanguages({}).prettier).toMatchObject({ prefer: false, runOnSave: false, path: "" });
|
||||
expect(
|
||||
@@ -69,6 +77,7 @@ describe("language pages", () => {
|
||||
"Node.js",
|
||||
"Python Interpreter",
|
||||
"Prettier",
|
||||
"Ruby",
|
||||
"Rust",
|
||||
"TypeScript",
|
||||
]);
|
||||
|
||||
@@ -109,6 +109,28 @@ export function NodePage() {
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ruby
|
||||
|
||||
export function RubyPage() {
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<Section>
|
||||
<SdkRow
|
||||
k="languages.ruby.interpreter"
|
||||
label="Ruby interpreter"
|
||||
kind="ruby"
|
||||
emptyLabel="Auto"
|
||||
emptyDetail=".ruby-version / mise / asdf, else ruby on PATH"
|
||||
browseTitle="Select Ruby Interpreter"
|
||||
aria-label="Ruby interpreter"
|
||||
hint="Runs the ruby, rake and rspec configurations and lists the standard library and gems under External Libraries. Installs from rbenv, rvm, chruby, mise and asdf are detected."
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rust
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Languages & Frameworks › Go / Java / Node.js / Rust / TypeScript. Pages live in
|
||||
* `../languages/`; settings are `languages.*` / `projectStructure.*`.
|
||||
*/
|
||||
import { GoPage, JavaPage, NodePage, RustPage, TypeScriptPage } from "../languages/LanguagePages";
|
||||
import { GoPage, JavaPage, NodePage, RubyPage, RustPage, TypeScriptPage } from "../languages/LanguagePages";
|
||||
import { PrettierPage } from "../languages/PrettierPage";
|
||||
import { ProjectStructurePage } from "../languages/ProjectStructurePage";
|
||||
import { PythonInterpreterPage } from "../languages/PythonInterpreterPage";
|
||||
@@ -118,6 +118,16 @@ export const LANGUAGE_PAGES: PageSpec[] = [
|
||||
keywords: ["formatter", "prettier", "markdown", "yaml", "css", "format on save", "node_modules"],
|
||||
component: PrettierPage,
|
||||
},
|
||||
{
|
||||
id: "languages.ruby",
|
||||
projectScope: true,
|
||||
defaultScope: "project",
|
||||
title: "Ruby",
|
||||
group: LANGUAGES_GROUP,
|
||||
order: 403.8,
|
||||
keywords: ["ruby interpreter", "rbenv", "rvm", "chruby", "bundler", "gems", "rake", "rspec"],
|
||||
component: RubyPage,
|
||||
},
|
||||
{
|
||||
id: "languages.rust",
|
||||
projectScope: true,
|
||||
|
||||
@@ -27,6 +27,7 @@ const DEFAULT_GO_BUILD_TAGS: GoBuildTags = {
|
||||
|
||||
export interface LanguagesValid {
|
||||
python: { interpreter: string; activateVirtualenv: boolean };
|
||||
ruby: { interpreter: string };
|
||||
node: { interpreter: string; packageManager: "" | "npm" | "pnpm" | "yarn" | "bun" };
|
||||
rust: {
|
||||
toolchain: string;
|
||||
@@ -77,6 +78,7 @@ export type FolderListKey =
|
||||
|
||||
export const DEFAULT_LANGUAGES: LanguagesValid = {
|
||||
python: { interpreter: "", activateVirtualenv: true },
|
||||
ruby: { interpreter: "" },
|
||||
node: { interpreter: "", packageManager: "" },
|
||||
rust: { toolchain: "", stdlib: "", checkOnSave: true, checkCommand: "check", rustfmtArgs: "" },
|
||||
go: { goroot: "", gopath: "", buildTags: DEFAULT_GO_BUILD_TAGS, modules: { env: {}, autoVendor: true } },
|
||||
@@ -158,6 +160,7 @@ export function validateLanguages(raw: unknown): LanguagesValid {
|
||||
const d = DEFAULT_LANGUAGES;
|
||||
const l = obj(raw);
|
||||
const py = obj(l.python);
|
||||
const ruby = obj(l.ruby);
|
||||
const node = obj(l.node);
|
||||
const rust = obj(l.rust);
|
||||
const go = obj(l.go);
|
||||
@@ -173,6 +176,7 @@ export function validateLanguages(raw: unknown): LanguagesValid {
|
||||
interpreter: text(py.interpreter, d.python.interpreter),
|
||||
activateVirtualenv: bool(py.activateVirtualenv, d.python.activateVirtualenv),
|
||||
},
|
||||
ruby: { interpreter: text(ruby.interpreter, d.ruby.interpreter) },
|
||||
node: {
|
||||
interpreter: text(node.interpreter, d.node.interpreter),
|
||||
packageManager: oneOf(node.packageManager, ["", "npm", "pnpm", "yarn", "bun"] as const, d.node.packageManager),
|
||||
|
||||
@@ -4876,6 +4876,30 @@
|
||||
"Directory, file or test id; empty runs the whole suite": "目录、文件或测试 ID;留空则运行整个测试套件",
|
||||
"pytest options": "pytest 选项",
|
||||
"e.g. -x -q --tb=short": "例如 -x -q --tb=short",
|
||||
"Ruby": "Ruby 语言",
|
||||
"Run a Ruby script": "运行 Ruby 脚本",
|
||||
"Ruby script": "Ruby 脚本",
|
||||
"e.g. bin/rails or lib/app.rb": "例如 bin/rails 或 lib/app.rb",
|
||||
"Script arguments": "脚本实参",
|
||||
"e.g. server -p 3000": "例如 server -p 3000",
|
||||
"Ruby arguments": "Ruby 实参",
|
||||
"e.g. -Ilib -w": "例如 -Ilib -w",
|
||||
"Run with Bundler (bundle exec)": "通过 Bundler 运行(bundle exec)",
|
||||
"Launcher": "启动器",
|
||||
"Path of the tool (bundle when run with Bundler); empty uses the interpreter from Settings › Ruby": "工具的路径(通过 Bundler 运行时为 bundle);留空则使用“设置 › Ruby”中的解释器",
|
||||
"Rake": "Rake 任务",
|
||||
"Run a Rake task": "运行 Rake 任务",
|
||||
"Task name with arguments, e.g. db:migrate or test TEST=test/a_test.rb; empty runs the default task": "任务名及其实参,例如 db:migrate 或 test TEST=test/a_test.rb;留空则运行默认任务",
|
||||
"Rake options": "Rake 选项",
|
||||
"e.g. --trace": "例如 --trace",
|
||||
"RSpec": "RSpec 测试",
|
||||
"Run RSpec examples": "运行 RSpec 示例",
|
||||
"e.g. spec": "例如 spec",
|
||||
"Directory, file or file:line; empty runs the whole suite": "目录、文件或 文件:行号;留空则运行整个测试套件",
|
||||
"RSpec options": "RSpec 选项",
|
||||
"e.g. --format documentation --fail-fast": "例如 --format documentation --fail-fast",
|
||||
"Ruby interpreter": "Ruby 解释器",
|
||||
"Runs the ruby, rake and rspec configurations and lists the standard library and gems under External Libraries. Installs from rbenv, rvm, chruby, mise and asdf are detected.": "用于运行 ruby、rake 和 rspec 配置,并在“外部库”下列出标准库和 gem。会检测 rbenv、rvm、chruby、mise 和 asdf 安装的版本。",
|
||||
"Monospaced": "等宽",
|
||||
"Proportional": "非等宽",
|
||||
"Loading system fonts…": "正在加载系统字体…",
|
||||
|
||||
+1
-1
@@ -874,7 +874,7 @@ const EXT_ICONS = table({
|
||||
"file-swift": "swift swiftinterface",
|
||||
"file-go": "go",
|
||||
"file-rust": "rs",
|
||||
"file-ruby": "rb rake gemspec ru rbi jbuilder podspec",
|
||||
"file-ruby": "rb rake gemspec ru rbi jbuilder podspec erb rhtml",
|
||||
"file-lua": "lua luau rockspec",
|
||||
"file-zig": "zig zon",
|
||||
"file-gdscript": "gd",
|
||||
|
||||
Reference in New Issue
Block a user