Skip to content
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

Add support for Ruby LSP as a built-in add-on #630

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open

Conversation

searls
Copy link
Contributor

@searls searls commented May 17, 2024

Release plan

  • Add a Ruby LSP addon as a formatter and a (pull) diagnostic provider
  • Add a test for the addon, using a modified test server helper that relies on ruby_lsp internals
  • Drop support for Ruby 2.7, so we can test the LSP plugin
  • Get CI passing again
  • Validate this all works in VS Code with the latest version of Ruby LSP
  • Cut a patch release

Background

After talking to @vinistock at Kaigi, I want to take a stab at advertising a working Ruby LSP addon from within the gem so that Ruby LSP users can get standard support without running the LSP themselves (or installing a custom extension, like our VS Code extension

I'm trying and failing to get this end-to-end working in an example app, so I'm trying to take a step back and just get this test passing. The result's response is nil, but (one assumes?) it shouldn't be. More importantly, WrapsBuiltinLspStandardizer#run_formatting isn't being invoked, so my attempts to set the formatter to `standard is probably not working, either.

To replicate the failure, you should be able to run:

$ bundle
$ bundle exec m test/ruby_lsp_addon_test.rb

The result's response is nil, but (one assumes) it shouldn't be

$ bundle exec m test/ruby_lsp_addon_test.rb
@st0012
Copy link

st0012 commented May 17, 2024

It's failing because Ruby LSP doesn't format files outside of the workspace path. You can workaround that with:

-    with_server(source) do |server, uri|
+    with_server(source) do |server, _uri|
+      uri = URI(File.join(server.global_state.workspace_path, 'fake.rb'))
       server.process_message(
         id: 1,
-        method: "textDocument/formatting",
-        params: {textDocument: {uri: uri}, position: {line: 0, character: 0}}
+        method: 'textDocument/formatting',
+        params: { textDocument: { uri: uri },
+                  position: { line: 0, character: 0 } }
       )

However, it'd then actually try to run formatting with that file, which will result to another error since the file doesn't actually exist.

So you'll likely need additional setup to:

  • Create an actual file with the target source (maybe under path_to_standard/tmp/fake.rb)?
  • Run the test against that path
  • Remove the file / or tmp/ altogether

@searls
Copy link
Contributor Author

searls commented May 17, 2024

@st0012 thanks Stan! Got through that and have it formatting correctly now.

Question: can I safely rely on Dir.pwd being the workspace directory, as I do with my custom extension?

Question: this test is now failing b/c the range being returned ends on line 19, column 19, even though the source string is only 2 lines long and that second line is only 5 or 6 characters. Since the addon is not providing the range, is it a bug?

@searls
Copy link
Contributor Author

searls commented May 20, 2024

I'm unsure the cause of the test failure this introduces, but just looking at the addon stuff, does this look right? @vinistock, @st0012?

@searls
Copy link
Contributor Author

searls commented May 22, 2024

Looking into this failure, I have very few good answers.

AFAICT, if one executes require "ruby-lsp/internal" in our test suite, then the CLI test will start failing, with the odd behavior of suddenly being incapable of autofixing Lint/TrailingCommaInAttributeDeclaration

The test will print this to stderr:

tmp/cli_test/subject.rb:18:19: Lint/TrailingCommaInAttributeDeclaration: Avoid leaving a trailing comma in attribute declarations.

And then the test will fail because the shelled-out run will exit non-zero.

This is a bit of a mystery to me. Like, the best explanation I can imagine is that loading ruby_lsp/internal causes some Rubocop something-or-other to be loaded which in turn mutates the default configuration set of Lint/TrailingCommaInAttributeDeclaration so as to flag it as not autocorrectable?

Extremely odd failure

@searls searls changed the title Spike: Ruby LSP Addon Add support for Ruby LSP as a built-in add-on May 23, 2024
@searls
Copy link
Contributor Author

searls commented May 23, 2024

Hey @vinistock @st0012, I'm trying to validate this actually works by setting up a project pointing at my local copy using:

  gem "standard", path: "/path/to/standardrb/standard"

But Ruby LSP doesn't seem to pick it up as a formatter/linter option.

screenshot-2024-05-23-11h16m16s

(If I puts something at the end of the addon's activate method, I'll see it blow up in the Ruby LSP output tab, so I know it's being required, however)

Question: do you have any idea why this might not be working when the plugin is a local path-sourced gem?

Meanwhile, if I change my dependency to look at this github branch:

gem "standard", github: "standardrb/standard", branch: "ruby-lsp-spike"

And then I check the output tab of VS Code, I get a stack trace without an error message:

2024-05-23 11:30:02.677 [info] (grog) /Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/standard/plugin/creates_runner_context.rb:7:in `call'

/Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/standard/plugin/merges_plugins_into_rubocop_config.rb:29:in `call'

/Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/standard/plugin/combines_plugin_configs.rb:11:in `call'

/Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/standard/creates_config_store.rb:22:in `block in call'

<internal:kernel>:90:in `tap'

/Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/standard/creates_config_store.rb:19:in `call'

/Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/standard/builds_config.rb:32:in `call'

/Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/ruby_lsp/standard/wraps_built_in_lsp_standardizer.rb:15:in `init!'

/Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/ruby_lsp/standard/wraps_built_in_lsp_standardizer.rb:11:in `initialize'

/Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/ruby_lsp/standard/addon.rb:15:in `new'

/Users/justin/.rbenv/versions/3.3.0/lib/ruby/gems/3.3.0/bundler/gems/standard-e7d5e49aafd2/lib/ruby_lsp/standard/addon.rb:15:in `activate'

So I'm kind of at a loss as to how to actually test the plugin locally, as a result.

@st0012
Copy link

st0012 commented May 23, 2024

It looks like the error was caused by Standard::VERSION not being defined when it's called here.
Adding require_relative "../../standard/version" makes everything works as expected.

Screenshot 2024-05-23 at 22 31 16

And for some reason the addon's error reporting mechanism didn't work as expected as the error message is not displayed. I'll give it a look when I'm back to work.

@andyw8
Copy link
Contributor

andyw8 commented May 23, 2024

And for some reason the addon's error reporting mechanism didn't work as expected as the error message is not displayed.

This should be solved by Shopify/ruby-lsp#2085

@searls
Copy link
Contributor Author

searls commented May 24, 2024

Thank you both for the above. I fixed the constant not defined error, but even after clearing it I'm seeing the same behavior where the plugin isn't recognized as a formatter

Setting value of "rubyLsp.formatter" to "standard" yields the problem:

> Value is not accepted. Valid values: "auto", "rubocop", "syntax_tree", "none".

Additionally, when I choose "format document" from the command pallete, I get "There is no formatter for 'ruby' files installed."

Here's a sample repo to demo the config:
https://github.com/searls/ruby-lsp-standard-example

@andyw8
Copy link
Contributor

andyw8 commented May 24, 2024

@searls You probably need to add standard to the enum here.

But the extension shouldn't really have to know about specific addons, so we probably need to re-think this approach.

@searls
Copy link
Contributor Author

searls commented May 25, 2024

@andyw8 ahh, yes, if it's just hard-coded now that won't really scale to third-party add-ons. I was assuming the extension was phoning home to the LSP and then updating the configuration schema dynamically (somehow? Think I was conflating it with WorkspaceConfiguration API, which I doubt can be used for that purpose.)

It might be necessary to just relax that configuration property in the schema to allow it to be set to any value?

@andyw8
Copy link
Contributor

andyw8 commented May 27, 2024

Looking into some options: Shopify/ruby-lsp#2092

Copy link

@vinistock vinistock left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

end

def init!
@config = ::Standard::BuildsConfig.new.call(ARGV)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ARGV here is going to be the set of arguments passed to the ruby-lsp executable, which are likely not recognized by standard. Could that lead to any issues? Would it be better to pass an empty array every time?

@config = ::Standard::BuildsConfig.new.call(ARGV)
@standardizer = ::Standard::Lsp::Standardizer.new(
@config,
::Standard::Lsp::Logger.new

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this logger have any chance to print to stdout? In the LSP context, nothing can be printed to STDOUT or else is breaks editor/server communication. Everything has to be printed to STDERR only.

end

def run_formatting(uri, document)
@standardizer.format(uri_to_path(uri), document.source)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a helper that patches a new method into URI objects to get the correct file system paths, which has some fixes for Windows (the URI gem has some bugs).

Suggested change
@standardizer.format(uri_to_path(uri), document.source)
@standardizer.format(uri.to_standardized_path, document.source)

There's only one caveat. If you created a Ruby file and it is currently unsaved, you can invoke formatting manually. In those cases, to_standardized_path returns nil. I'm not sure how Standard will handle that.

end

def run_diagnostic(uri, document)
offenses = @standardizer.offenses(uri_to_path(uri), document.source)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
offenses = @standardizer.offenses(uri_to_path(uri), document.source)
offenses = @standardizer.offenses(uri.to_standardized_path, document.source)

when "refactor", "info"
RubyLsp::Constant::DiagnosticSeverity::HINT
else # the above cases fully cover what RuboCop sends at this time
logger.puts "Unknown severity: #{severity.inspect}"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this print to STDOUT? If so, it will throw the LSP into an infinite loop.

start: RubyLsp::Interface::Position.new(line: loc[:start_line] - 1, character: loc[:start_column] - 1),
end: RubyLsp::Interface::Position.new(line: loc[:last_line] - 1, character: loc[:last_column] - 1)
)
# TODO: do I need something like this?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to support quickfixes through code actions, then yes you'll need to provide those as part of data.

But you can always start without it and add later.


# lifted from:
# https://github.com/Shopify/ruby-lsp/blob/4c1906172add4d5c39c35d3396aa29c768bfb898/lib/ruby_lsp/requests/support/rubocop_diagnostic.rb#L84
def code_description(cop_name)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should warn you that a contributor is looking into changing this slightly to allow for RuboCop extensions to also have their documentation URLs displayed on diagnostics.

Likely not something that needs to be addressed here, just an FYI Shopify/ruby-lsp#2076.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants