From 8daa5a2ef2227703f1e456c00c5309efeac9524f Mon Sep 17 00:00:00 2001 From: Hungle2911 <122929293+Hungle2911@users.noreply.github.com> Date: Tue, 4 Feb 2025 18:23:41 -0500 Subject: [PATCH 1/9] add --new-addon option for the ruby-lsp executable to generate new ruby-lsp add-on gem --- exe/ruby-lsp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/exe/ruby-lsp b/exe/ruby-lsp index 1c2833c2d8..5b787314d5 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", + ) do |name| + options[:new_addon] = name + end + opts.on("--doctor", "Run troubleshooting steps") do options[:doctor] = true end From cbbcb2b7f204a117b10056b5866af8dc1a085718 Mon Sep 17 00:00:00 2001 From: Hungle2911 <122929293+Hungle2911@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:18:34 -0500 Subject: [PATCH 2/9] implement logic for generator --- exe/ruby-lsp | 5 +- lib/ruby_lsp/generator.rb | 106 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 lib/ruby_lsp/generator.rb diff --git a/exe/ruby-lsp b/exe/ruby-lsp index 5b787314d5..6210656b17 100755 --- a/exe/ruby-lsp +++ b/exe/ruby-lsp @@ -31,7 +31,7 @@ parser = OptionParser.new do |opts| opts.on( "--new-addon [NAME]", - "Create a new Ruby LSP add-on", + "Create a new Ruby LSP add-on gem", ) do |name| options[:new_addon] = name end @@ -154,6 +154,9 @@ if options[:doctor] return end +if options[:new_addon] + +end # Ensure all output goes out stderr by default to allow puts/p/pp to work # without specifying output device. $> = $stderr diff --git a/lib/ruby_lsp/generator.rb b/lib/ruby_lsp/generator.rb new file mode 100644 index 0000000000..780571678c --- /dev/null +++ b/lib/ruby_lsp/generator.rb @@ -0,0 +1,106 @@ +# 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? + create_addon_files + else + create_new_gem + end + end + + private + + sig { returns(T::Boolean) } + def inside_existing_project? + File.exist?("Gemfile") + 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, + # typed: strict + # frozen_string_literal: true + + require 'sorbet-runtime' + + module RubyLsp + module #{camelize(addon_name)} + class Addon + extend T::Sig + + # Your add-on logic here + end + end + end + RUBY + ) + + # Create a test file + test_dir = T.let("test/ruby_lsp/#{addon_name}", String) + FileUtils.mkdir_p(test_dir) + File.write( + "#{test_dir}/addon_test.rb", + <<~RUBY, + # typed: strict + # frozen_string_literal: true + + require "test_helper" + + module RubyLsp + module #{camelize(addon_name)} + class AddonTest < Minitest::Test + extend T::Sig + + sig { void } + def test_example + assert true + end + end + end + end + RUBY + ) + + puts "Add-on '#{addon_name}' created successfully!" + end + + sig { void } + def create_new_gem + puts "No existing project found. Would you like to create a new gem? (y/n)" + response = T.let(gets.chomp.downcase, String) + + if response == "y" + addon_name = T.must(@addon_name) + system("bundle gem #{addon_name}") + Dir.chdir(addon_name) do + create_addon_files + end + else + puts "Exiting without creating a new gem." + end + end + + sig { params(string: String).returns(String) } + def camelize(string) + string.split("_").map(&:capitalize).join + end + end +end From 4bb6f6a4eb48e10d7987411f68cda7bad3008700 Mon Sep 17 00:00:00 2001 From: Hungle2911 <122929293+Hungle2911@users.noreply.github.com> Date: Tue, 11 Feb 2025 13:41:49 -0500 Subject: [PATCH 3/9] chores: remove default sorbet typing --- lib/ruby_lsp/generator.rb | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/lib/ruby_lsp/generator.rb b/lib/ruby_lsp/generator.rb index 780571678c..34109b9b64 100644 --- a/lib/ruby_lsp/generator.rb +++ b/lib/ruby_lsp/generator.rb @@ -36,15 +36,11 @@ def create_addon_files File.write( "#{addon_dir}/addon.rb", <<~RUBY, - # typed: strict # frozen_string_literal: true - require 'sorbet-runtime' - module RubyLsp module #{camelize(addon_name)} class Addon - extend T::Sig # Your add-on logic here end @@ -59,7 +55,6 @@ class Addon File.write( "#{test_dir}/addon_test.rb", <<~RUBY, - # typed: strict # frozen_string_literal: true require "test_helper" @@ -67,9 +62,7 @@ class Addon module RubyLsp module #{camelize(addon_name)} class AddonTest < Minitest::Test - extend T::Sig - sig { void } def test_example assert true end @@ -84,17 +77,10 @@ def test_example sig { void } def create_new_gem - puts "No existing project found. Would you like to create a new gem? (y/n)" - response = T.let(gets.chomp.downcase, String) - - if response == "y" - addon_name = T.must(@addon_name) - system("bundle gem #{addon_name}") - Dir.chdir(addon_name) do - create_addon_files - end - else - puts "Exiting without creating a new gem." + addon_name = T.must(@addon_name) + system("bundle gem #{addon_name}") + Dir.chdir(addon_name) do + create_addon_files end end From f12f86ec340d3752be5eabd15055c06ea921a537 Mon Sep 17 00:00:00 2001 From: Hungle2911 <122929293+Hungle2911@users.noreply.github.com> Date: Tue, 11 Feb 2025 14:30:06 -0500 Subject: [PATCH 4/9] add more boilerplate code to addon.rb template --- lib/ruby_lsp/generator.rb | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/lib/ruby_lsp/generator.rb b/lib/ruby_lsp/generator.rb index 34109b9b64..d72f8da531 100644 --- a/lib/ruby_lsp/generator.rb +++ b/lib/ruby_lsp/generator.rb @@ -26,6 +26,11 @@ def inside_existing_project? File.exist?("Gemfile") end + sig { params(string: String).returns(String) } + def camelize(string) + string.split("_").map(&:capitalize).join + end + sig { void } def create_addon_files addon_name = T.must(@addon_name) @@ -40,9 +45,27 @@ def create_addon_files module RubyLsp module #{camelize(addon_name)} - class Addon + 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 - # Your add-on logic here + # 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 @@ -83,10 +106,5 @@ def create_new_gem create_addon_files end end - - sig { params(string: String).returns(String) } - def camelize(string) - string.split("_").map(&:capitalize).join - end end end From 8f2ccb6a896e14db3a3fb5e854e1191549dec7b6 Mon Sep 17 00:00:00 2001 From: Hungle2911 <122929293+Hungle2911@users.noreply.github.com> Date: Tue, 11 Feb 2025 16:51:40 -0500 Subject: [PATCH 5/9] feat: detect test framework and set up test accordingly --- lib/ruby_lsp/generator.rb | 86 +++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 16 deletions(-) diff --git a/lib/ruby_lsp/generator.rb b/lib/ruby_lsp/generator.rb index d72f8da531..1f23b437ce 100644 --- a/lib/ruby_lsp/generator.rb +++ b/lib/ruby_lsp/generator.rb @@ -72,12 +72,63 @@ def version RUBY ) - # Create a test file - test_dir = T.let("test/ruby_lsp/#{addon_name}", String) - FileUtils.mkdir_p(test_dir) - File.write( - "#{test_dir}/addon_test.rb", - <<~RUBY, + create_test_file + + puts "Add-on '#{addon_name}' created successfully! Please follow guidelines on https://shopify.github.io/ruby-lsp/add-ons.html + for best practice" + end + + sig { void } + def create_new_gem + addon_name = T.must(@addon_name) + system("bundle gem #{addon_name}") + Dir.chdir(addon_name) do + create_addon_files + end + 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" @@ -85,7 +136,6 @@ def version module RubyLsp module #{camelize(addon_name)} class AddonTest < Minitest::Test - def test_example assert true end @@ -93,17 +143,21 @@ def test_example end end RUBY - ) + when :test_unit + FileUtils.mkdir_p(test_dir) + File.write("#{test_dir}/addon_test.rb", <<~RUBY) + # frozen_string_literal: true - puts "Add-on '#{addon_name}' created successfully!" - end + require "test_helper" - sig { void } - def create_new_gem - addon_name = T.must(@addon_name) - system("bundle gem #{addon_name}") - Dir.chdir(addon_name) do - create_addon_files + class AddonTest < Test::Unit::TestCase + def test_example + assert true + end + end + RUBY + else + raise "Unsupported test framework: #{test_framework}" end end end From 6649f1fd13a2d841da9df7a1031d62fe73a11042 Mon Sep 17 00:00:00 2001 From: Hungle2911 <122929293+Hungle2911@users.noreply.github.com> Date: Fri, 14 Feb 2025 17:29:05 -0500 Subject: [PATCH 6/9] connect generator with command --- exe/ruby-lsp | 4 +++- lib/ruby_lsp/generator.rb | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/exe/ruby-lsp b/exe/ruby-lsp index 6210656b17..4e35ec4766 100755 --- a/exe/ruby-lsp +++ b/exe/ruby-lsp @@ -155,7 +155,9 @@ if options[:doctor] 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. diff --git a/lib/ruby_lsp/generator.rb b/lib/ruby_lsp/generator.rb index 1f23b437ce..40a1658738 100644 --- a/lib/ruby_lsp/generator.rb +++ b/lib/ruby_lsp/generator.rb @@ -13,8 +13,10 @@ def initialize(addon_name) 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 From f97f89915fc832e298c2e8e0d314c2020ce3f5c2 Mon Sep 17 00:00:00 2001 From: Hungle2911 <122929293+Hungle2911@users.noreply.github.com> Date: Tue, 18 Feb 2025 14:50:52 -0500 Subject: [PATCH 7/9] Feat: add ruby-lsp as development dependency in Gemfile --- lib/ruby_lsp/generator.rb | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/ruby_lsp/generator.rb b/lib/ruby_lsp/generator.rb index 40a1658738..71e15e51ac 100644 --- a/lib/ruby_lsp/generator.rb +++ b/lib/ruby_lsp/generator.rb @@ -45,6 +45,8 @@ def create_addon_files <<~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 @@ -84,11 +86,35 @@ def version 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") From 051564c0cbd2b4b761b762223d2d70499df1b815 Mon Sep 17 00:00:00 2001 From: Hungle2911 <122929293+Hungle2911@users.noreply.github.com> Date: Wed, 19 Feb 2025 00:02:29 -0500 Subject: [PATCH 8/9] Docs: add documentation to introduce the new generator feature --- jekyll/add-ons.markdown | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 From 6f4185227764171575f500937c8305f164848869 Mon Sep 17 00:00:00 2001 From: Hungle2911 <122929293+Hungle2911@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:50:26 -0500 Subject: [PATCH 9/9] feat: improve camelize method to generate correct module name in template --- lib/ruby_lsp/generator.rb | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/ruby_lsp/generator.rb b/lib/ruby_lsp/generator.rb index 71e15e51ac..3690d17924 100644 --- a/lib/ruby_lsp/generator.rb +++ b/lib/ruby_lsp/generator.rb @@ -30,7 +30,13 @@ def inside_existing_project? sig { params(string: String).returns(String) } def camelize(string) - string.split("_").map(&:capitalize).join + return string if string == "ruby-lsp" + + string + .gsub("ruby-lsp-", "") + .split(/[-_]/) + .map(&:capitalize) + .join end sig { void } @@ -78,8 +84,7 @@ def version create_test_file - puts "Add-on '#{addon_name}' created successfully! Please follow guidelines on https://shopify.github.io/ruby-lsp/add-ons.html - for best practice" + puts "Add-on '#{addon_name}' created successfully! Please follow guidelines on https://shopify.github.io/ruby-lsp/add-ons.html" end sig { void }