diff --git a/exe/ruby-lsp b/exe/ruby-lsp index 1c2833c2d8..4e35ec4766 100755 --- a/exe/ruby-lsp +++ b/exe/ruby-lsp @@ -29,6 +29,13 @@ parser = OptionParser.new do |opts| options[:branch] = branch end + opts.on( + "--new-addon [NAME]", + "Create a new Ruby LSP add-on gem", + ) do |name| + options[:new_addon] = name + end + opts.on("--doctor", "Run troubleshooting steps") do options[:doctor] = true end @@ -147,6 +154,11 @@ if options[:doctor] return end +if options[:new_addon] + require "ruby_lsp/generator" + RubyLsp::Generator.new(options[:new_addon]).run + exit(0) +end # Ensure all output goes out stderr by default to allow puts/p/pp to work # without specifying output device. $> = $stderr diff --git a/jekyll/add-ons.markdown b/jekyll/add-ons.markdown index ea0f9685fd..ca1ec2792c 100644 --- a/jekyll/add-ons.markdown +++ b/jekyll/add-ons.markdown @@ -55,6 +55,19 @@ authors to benefit from types declared by the Ruby LSP. As an example, check out [Ruby LSP Rails](https://github.com/Shopify/ruby-lsp-rails), which is a Ruby LSP add-on to provide Rails related features. +### Creating a New Add-on + +The `ruby-lsp` executable now includes a `--new-addon` option to help you quickly create new Ruby LSP add-ons. This feature is designed to work in two scenarios: + +1. **Inside an existing project**: If you're already working in a project with a `Gemfile`, the tool will add the necessary files and directory structure for your new add-on. +2. **Outside a project**: If you're not in a project, the tool will help you create a new gem and then set up the add-on inside it. + +To create a new add-on, run the following command: + +```bash +ruby-lsp --new-addon ADDON_NAME +``` + ### Activating the add-on The Ruby LSP discovers add-ons based on the existence of an `addon.rb` file placed inside a `ruby_lsp` folder. For diff --git a/lib/ruby_lsp/generator.rb b/lib/ruby_lsp/generator.rb new file mode 100644 index 0000000000..3690d17924 --- /dev/null +++ b/lib/ruby_lsp/generator.rb @@ -0,0 +1,197 @@ +# typed: strict +# frozen_string_literal: true + +module RubyLsp + class Generator + extend T::Sig + + sig { params(addon_name: T.nilable(String)).void } + def initialize(addon_name) + @addon_name = T.let(addon_name, T.nilable(String)) + end + + sig { void } + def run + if inside_existing_project? + puts "Inside existing project. Creating add-on files..." + create_addon_files + else + puts "Not inside existing project. Prompting to create new gem..." + create_new_gem + end + end + + private + + sig { returns(T::Boolean) } + def inside_existing_project? + File.exist?("Gemfile") + end + + sig { params(string: String).returns(String) } + def camelize(string) + return string if string == "ruby-lsp" + + string + .gsub("ruby-lsp-", "") + .split(/[-_]/) + .map(&:capitalize) + .join + end + + sig { void } + def create_addon_files + addon_name = T.must(@addon_name) + addon_dir = T.let("lib/ruby_lsp/#{addon_name}", String) + FileUtils.mkdir_p(addon_dir) + + # Create addon.rb + File.write( + "#{addon_dir}/addon.rb", + <<~RUBY, + # frozen_string_literal: true + + RubyLsp::Addon.depend_on_ruby_lsp!(">= 0.23.1", "< 0.24") + + module RubyLsp + module #{camelize(addon_name)} + class Addon < ::RubyLsp::Addon + # Performs any activation that needs to happen once when the language server is booted + def activate(global_state, message_queue) + # Add your logic here + end + + # Performs any cleanup when shutting down the server, like terminating a subprocess + def deactivate + # Add your logic here + end + + # Returns the name of the add-on + def name + "Ruby LSP My Gem" + end + + # Defining a version for the add-on is mandatory. This version doesn't necessarily need to match the version of + # the gem it belongs to + def version + "0.1.0" + end + end + end + end + RUBY + ) + + create_test_file + + puts "Add-on '#{addon_name}' created successfully! Please follow guidelines on https://shopify.github.io/ruby-lsp/add-ons.html" + end + + sig { void } + def create_new_gem + addon_name = T.must(@addon_name) + system("bundle gem #{addon_name}") + add_ruby_lsp_to_gemfile + Dir.chdir(addon_name) do + create_addon_files + end + end + + sig { void } + def add_ruby_lsp_to_gemfile + gemfile_path = "Gemfile" + + unless File.exist?(gemfile_path) + puts "Gemfile not found. Please ensure you are in the root directory of your gem." + return + end + + gemfile_content = File.read(gemfile_path) + + if gemfile_content.include?("gem 'ruby-lsp'") || gemfile_content.include?('gem "ruby-lsp"') + puts "ruby-lsp is already in the Gemfile." + return + end + + updated_content = gemfile_content + "\ngem \"ruby-lsp\", \">= 0.23.1\", group: :development\n" + + File.write(gemfile_path, updated_content) + + puts "Added ruby-lsp as a development dependency to the Gemfile." + end + + sig { returns(Symbol) } + def check_test_framework + if File.exist?("Gemfile") + gemfile_content = T.let(File.read("Gemfile"), String) + if gemfile_content.include?("rspec") + :rspec + elsif gemfile_content.include?("minitest") + :minitest + elsif gemfile_content.include?("test-unit") + :test_unit + else + :minitest + end + else + :minitest + end + end + + sig { void } + def create_test_file + addon_name = T.must(@addon_name) + test_dir = "test/ruby_lsp/#{@addon_name}" + spec_test_dir = "spec/ruby_lsp/#{@addon_name}" + test_framework = check_test_framework + + case test_framework + when :rspec + FileUtils.mkdir_p(spec_test_dir) + File.write("#{spec_test_dir}/addon_spec.rb", <<~RUBY) + # frozen_string_literal: true + + require "spec_helper" + + RSpec.describe RubyLsp::#{camelize(addon_name)}::Addon do + it "does something useful" do + expect(true).to eq(true) + end + end + RUBY + when :minitest + FileUtils.mkdir_p(test_dir) + File.write("#{test_dir}/addon_test.rb", <<~RUBY) + # frozen_string_literal: true + + require "test_helper" + + module RubyLsp + module #{camelize(addon_name)} + class AddonTest < Minitest::Test + def test_example + assert true + end + end + end + end + RUBY + when :test_unit + FileUtils.mkdir_p(test_dir) + File.write("#{test_dir}/addon_test.rb", <<~RUBY) + # frozen_string_literal: true + + require "test_helper" + + class AddonTest < Test::Unit::TestCase + def test_example + assert true + end + end + RUBY + else + raise "Unsupported test framework: #{test_framework}" + end + end + end +end