Skip to content

Add an add on generator #3217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
12 changes: 12 additions & 0 deletions exe/ruby-lsp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions jekyll/add-ons.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
197 changes: 197 additions & 0 deletions lib/ruby_lsp/generator.rb
Original file line number Diff line number Diff line change
@@ -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
Loading