Mastering Ruby Code Navigation: Ruby LSP Enhancements in the First Half of 2024
https://railsatscale.com/2024-07-18-mastering-ruby-code-navigation-major-enhancements-in-ruby-lsp-2024/By ksec at
stevebmark | 4 comments | 2 months ago
jaynetics | 1 comment | 2 months ago
I guess it depends on scale as well. The bigger the codebase, the nicer it is to find implementations and references automatically, and the bigger the company, the more likely they are to have good workarounds for the shortcomings of various language ecosystems or even dedicated dev experience teams, in which case Ruby's potential runtime intricacies might really start to weigh it down. Then again, there seems to be some movement away from that all-too-dynamic stuff e.g. in the rails codebase.
KronisLV | 1 comment | 2 months ago
Okay, this won't probably be 100% on topic, but in my experience how well these methods work is inversely proportional to the size of the codebase and also quite badly in some cases depending on how the code is written.
The other day I was working on a Java enterprise codebase (monolith, ~300K LoC) and I shouldn't have had that many issues navigating the codebase, however someone made a validator that resolves the logic to a specific class at runtime. So you'd basically have validator.validate(Object someObject). Suddenly if I wanted to find all of the places where validator.validate(MyObject myObject) is called, I could not do that easily and with naming like validator.validate(entity) all over the place, text search didn't help much either. They had an okay type system but chose not to use it, just because they wanted to pass in arbitrary objects, without regard for how easy finding usages will be in the future.
My point is, that you can probably write code that's easy or hard to navigate in any language (within reason), but I'll gladly take whatever tools I can get in any stack out there!
rtz121 | 0 comments | 2 months ago
The pains of not having multiple dispatch.
psychoslave | 0 comments | 2 months ago
That is, yesterday I spent the afternoon trying to follow a maze of signal observers in a Vue3 project. After a moment, not finding why the redirect to login page was nowhere to be found in the signal obsevers of the concerned transition, it finally reveled to be linked to a more broad route hook mechanism that the framework provides.
No meta programming is involved, be it in signal observers or route hook.
Regarding metaprogramming, Crystal keep only the parts that are straightfoward to deal with in static analysis, from what I grasped when skiming its elevator speech.
On Ruby side the great stuffs I like in it are more linked to the homogeneous approach it follows. Almost everything is an object and something like `42.extend(:custom•module).original•action` is possible due to that. It's not 100% pure object though, reserved keywords like `if` can not be used as objects, that is `if.class` is not valid.
paholg | 1 comment | 2 months ago
I wonder why no one's written a Ruby LSP with this approach rather than relying on static analysis.
grncdr | 0 comments | 2 months ago
eduction | 1 comment | 2 months ago
sapiogram | 1 comment | 2 months ago
brigandish | 0 comments | 2 months ago
Or is it that the original comment showed no knowledge of programming language history? It’s a tough choice!
simply-typed | 6 comments | 2 months ago
Sorbet may fix things, but at that point, just use a language with more mature tooling around types, like Python or TypeScript.
Dynamic types offer dubious marginal benefits but bring tons of downsides. The demonstrations in this article reflect that.
ecshafer | 2 comments | 2 months ago
maleldil | 1 comment | 2 months ago
That's not true. mypy and Pyright (the main type checkers) are very ahead of Ruby's Sorbet, and Python actually has proper ergonomic syntax to work with types, which Ruby doesn't. It's also seeing constant development to bring more powerful typing and syntax.
Also, typing has become part of the Python culture, which means most of the big libraries have type hints. It doesn't seem like that is the case with Ruby, where type hints aren't as prevalent. Even the core maintainers don't like the idea and want to keep it as dynamic as possible.
jshen | 2 comments | 2 months ago
ReleaseCandidat | 0 comments | 2 months ago
teaearlgraycold | 1 comment | 2 months ago
psychoslave | 2 comments | 2 months ago
thenipper | 0 comments | 2 months ago
maleldil | 0 comments | 2 months ago
barrenko | 1 comment | 2 months ago
simoncion | 1 comment | 2 months ago
Have you tried the 'solargraph' VS Code(ium) plugin? I've found it to be pretty good. If you haven't do give it a try, but do know that you need to build the YARD documentation for installed "gems" for Solargraph to not be dogshit.
As described here [0], you run 'yard gems' to build said documentation. Do note that the method of having this documentation always be generated doesn't seem to work with "gems" installed with Bundler. Very frustrating, that.
barrenko | 0 comments | 2 months ago
pjm331 | 1 comment | 2 months ago
ht85 | 3 comments | 2 months ago
The other camp "just hates it" because it "slows them down", as it seems they spend most of their time fighting the types but never get to the point where you get that huge return on investment.
psychoslave | 0 comments | 2 months ago
Unfortunately most of the time the result is at best needlessly cryptic, with zero thought about simplicity of use. Ruby has a reputation of taking care of this topic.
pmontra | 1 comment | 2 months ago
That's the great picture. Looking into the details I've been working with Perl, JavaScript, Python plus many other less common languages. I always had a preference for languages that hide complexity away from me.
Code completion really doesn't matter much to me. I've been working for maybe ten years with an emacs package that remembered words and attempted to autocomplete with the most used suffix. It worked surprisingly well.
simoncion | 1 comment | 2 months ago
If the type-checking system is decent, they also automatically catch a whole class of problems that will only show up at runtime, and may take years to surface. (Ask me about the error-handing code that detonated because it contained a typo in a Ruby class name, which was only discovered three years after it was introduced... when that code was actually exercised during an... exciting outage of a system it depended on.)
pansa2 | 1 comment | 2 months ago
Agreed. But that doesn't mean that every language needs to be statically-typed, which seems to be where we're heading nowadays.
IMO large and/or long-running projects should be written in languages with sound static type systems, not scripting languages with types tacked on. Conversely, I often work on projects which are neither large nor long-running - for those, a dynamically-typed scripting language works perfectly well.
> a typo in a Ruby class name, which was only discovered three years after it was introduced
So the code containing this typo was never tested? That's asking for trouble even if you have static typing.
simoncion | 1 comment | 2 months ago
The code absolutely was tested. However, (obviously) not every possible path through the code was tested.
Given a long-enough timeline, you will NEVER remember to test every little thing. Given sufficient code complexity, it can be next to impossible to actually exercise every code path with hand-written tests.
That's one of the troubles with large projects written scripting languages like Ruby... you have to write an assload of tests to replace what you get for free in languages (even "loosely"-typed languages like Erlang) that have pre-runtime type-checking (whether or not it's provided by a compiler).
> Conversely, I often work on projects which are neither large nor long-running - for those, a dynamically-typed scripting language works perfectly well.
Oh yeah, for small things such languages are A-OK. I 1000% agree with that. The big problem (that you may never encounter because I bet that you're smarter than the average unending parade of Project Managers) is how often small projects are forced into becoming big projects, and then huge projects as time goes on.
ninkendo | 1 comment | 2 months ago
Which means that IME a huge number of tests in my ruby code were doing things that a type checker does automatically: make sure that passing nil makes it return nil, making sure that passing an array does the right thing vs a string, etc etc etc.
I have very vivid memories of working in a large rails code base and having no idea whether my change would work until I actually exercised the code and realized I was completely messing up the contract that some method expected, so I had to slow down and write tests to validate my assumptions.
Fast forward to now, I work in a large Rust code base, and it seems like 99% of the time, if my code compiles, it works flawlessly the first time. So I concentrate on writing integration/functional tests, which give me a lot more bang for my buck than having to write hundreds of little unit tests. (I still write some unit tests, but the need for it is much less… the vast majority of things you’d write tests for in Ruby are things the type and borrow checkers handle for you in Rust.)
simoncion | 1 comment | 2 months ago
"Strong" as in "You easily burn 10x more time writing tests as you do code... and not because it's difficult to think through how to write good tests"? If so, yes.
"Good"? Hell, no! That's a bad reason.
> ...a huge number of tests in my ruby code were doing things that a type checker does automatically...
The folks I work with demand that we don't write these tests, so they don't get written. Guess how often code detonates in production because of things a typechecker would have caught... despite the enormous volume of test code.
To be crystal clear, I totally agree with your statements in this comment. I started my "career" with C++ and I'm so damn glad I did. Had I started with Ruby and Rails, I would have come to the conclusion I was far too damn stupid for this field and left to become a lawyer.
ninkendo | 0 comments | 2 months ago
pjm331 | 0 comments | 2 months ago
Agreed
> the other camp "just hates it" because it "slows them down"
I’ve no doubt there are some that fall into this category, but not everyone, not by a long shot.
cardy31 | 0 comments | 2 months ago
jshen | 1 comment | 2 months ago
viraptor | 1 comment | 2 months ago
Sorbet is very slow for large codebases. I keep checking it from time to time, but the biggest service I'm dealing with just fails to run through the initialisation. And that's ignoring the idea that someone will have to clean up the result. Not holding my breath for it to be functional in LSP situations.
pawelduda | 0 comments | 2 months ago
adamtaylor_13 | 1 comment | 2 months ago
chris12321 | 1 comment | 2 months ago
adamtaylor_13 | 1 comment | 2 months ago
People talk about the DSLs and conciseness, but frankly those don’t make/break Rails for me.
I’m curious to know what part of Rails wouldn’t be possible in something like Javascript.
chris12321 | 1 comment | 2 months ago
The same is true for defining associations between models. Consider the following model:
class User < ApplicationRecord
belongs_to :organization
scope :active, -> { where(active: true) }
end
You mentioned that DSLs aren't important, but the DSL for defining associations is extremely convenient. Adding the belongs_to line defines numerous methods on the User class, allowing you to do things like User.last.organization. The scope line dynamically defines the active method, allowing you to call User.active, which executes the code in the provided block. There are also DSLs for defining routes and migrations, which are extremely important when using Rails.Ruby's metaprogramming is also utilized in Rails controllers to load the correct code. For example, the index method in the UsersController knows to look for the views/users/index.html file because it can reflect on the method and the class to determine where to load the correct code using Rails' autoloader, Zeitwerk.
Ruby's open classes and monkey patching (something that should be done with extreme caution) allow ActiveSupport to add extremely useful methods to Ruby's base classes. This enables conveniences such as Date.today - 10.days or DateTime.now.beginning_of_day.
These are just a few examples that I can think of off the top of my head, but nearly every major convenience in Rails relies on the flexibility provided by Ruby. While it might be possible to reproduce many aspects of Rails in JavaScript, I don't believe you could replicate all of it with the same readability and intuitiveness that Rails offers.
dmux | 1 comment | 2 months ago
This can be accomplished in statically typed languages too. For example, Java has an ORM called Cayenne that generates models based on database tables. You can get similar functionality and ensure you're not accidentally calling something like `find_by_nam`
chris12321 | 0 comments | 2 months ago
And like I said above while you might be able to recreate some of Rails' functionality in other languages, you wouldn't be able to replicate all of it with the same fluidity and readability that you have in Rails
dabears | 0 comments | 2 months ago
I think it works well, is stable, and I would still reach for it over Shopify's LSP personally. Feel free to use it! Though as a heads up, I don't work in Ruby much anymore unfortunately so I'm not actively working on it or accepting issues.
mkl95 | 2 comments | 2 months ago
On the other hand, the Ruby community seems stuck in the 2000s, with some of the self inflicted Ruby pains and attitude being fairly similar to what some Python devs were doing in the late Python 2 era.
These days, I still like Ruby and I would enjoy building some little project with it, but I would rather make a living using other stuff.
bankcust08385 | 0 comments | 2 months ago
WizardClickBoy | 0 comments | 2 months ago
hstaab | 0 comments | 2 months ago
Alifatisk | 1 comment | 2 months ago
jaynetics | 1 comment | 2 months ago
Alifatisk | 1 comment | 2 months ago
brigandish | 1 comment | 2 months ago
Alifatisk | 0 comments | 2 months ago
hboon | 1 comment | 2 months ago
For everyone commenting about those and who haven't, I suggest trying RubyMine to set expectations on what is possible first.
simoncion | 1 comment | 2 months ago
In my professional experience with large, very long-running Ruby projects, RubyMine will at least once per session (and usually much more than once) give you search results that are no better (and sometimes worse) than if you'd just run `grep` on the codebase. (e.g. "I'm pretty sure ONE of these five hundred 'run' functions is the one you want! Oh, by the way, I also am showing you results from every .gem file I know about!")
It also has a bias for presenting results quickly and allowing you to make queries quickly... so quickly that it often returns partial results to queries with absolutely no outward indication that the results are incomplete.
Given what it seems that the other Ruby "Intellisense" plugins are having to do to well-understand real-world Ruby projects (especially those who use Rails and friends), I'm sure that a godawful lot of manual work and special-case knowledge has gone into RubyMine's Intellisense. But, it's still not nearly enough to be reliable on large projects.
hboon | 1 comment | 2 months ago
> I'm pretty sure ONE of these five hundred 'run' functions
Yes. Same thing with all unannotated dynamic languages. Like Smalltalk has a bazillion `#value` and `#value:`s, but there were options to scope it down. You can do that in RubyMine, but I forgot if you can do it by class/gems without manually creating a scope.
Just saying that such navigation isn't impossible with Ruby.
simoncion | 1 comment | 2 months ago
There might be one (one would think there WOULD be one), but if it does exist, it's so small and unobtrusive that I've never noticed it. My coworkers who use RubyMine every damn day don't seem to notice it either. If it exists, it needs to get much more obvious, IMO.
> ...but I forgot if you can do it by class/gems without manually creating a scope.
I know that my RubyMine-loving coworkers definitely don't do whatever this is and just power through the worse-than-grep results.
> Just saying that such navigation isn't impossible with Ruby.
Fully-automatic trouble-free navigation of sufficiently-large/complex projects is pretty much impossible with Ruby. (At least in a Ruby project that lacks comprehensive, correct type annotations.) I'm just saying that folks who might take RubyMine for a test drive on a small or toy project will be unlikely to get an accurate demonstration of its limitations.
(In my professional experience, RubyMine is not THAT much better than the 'solargraph' VSCode plugin.)
hboon | 1 comment | 2 months ago
You can choose and edit scopes with these 2 buttons — https://imgur.com/a/jZute6v
simoncion | 0 comments | 2 months ago
aantix | 0 comments | 2 months ago
Excited to try this to see if it measures up.
milofeynman | 1 comment | 2 months ago
Syntaf | 1 comment | 2 months ago
gls2ro | 1 comment | 2 months ago
Learning it is easy and using it is also easy if you stay away from: 1. Meta programming 2. Methods that are returning different results based on some conditions.
the second case it can be handled by Sorbet but it makes the type more complex.
I find this a nice side effect of Sorbet: - the moment I start fighting the types or feeling to add many T.any it is a signal to me that I should split my methods/objects
Some things that I found useful:
- I started to like T.enum, T.struct and somehow I am feeling them missing now in a normal Rails project.
- I also like T.let as it helps with object shapes and memoization.
- It also solves the inheritance and there is no need to discuss what exception to throw when you want to define a method that should be implemented in
- I would recommend the gem sorbet-results that adds a simple typed monad
- It helps a lot refactoring
To get the full benefits always try to make your files with typed:strict and of course use tapioca with Rails
Syntaf | 0 comments | 2 months ago
bankcust08385 | 0 comments | 2 months ago
msie | 0 comments | 2 months ago
lkrubner | 0 comments | 2 months ago
"Look up the source" in 2024 is a symptom of a wider problem. Ruby was innovative in the 1990s but it is starting to fall behind the times.
Those of us who are old enough to remember the euphoria that greeted the release of Ruby on Rails in 2004 are mostly surprised to see Ruby's current, limited market share. There was a moment when it seemed like Ruby was going to sweep the whole world of software. But that didn't happen. The mutability that made Ruby so fun also makes it difficult to maintain Ruby codebases over the long-term.
For awhile Ruby seemed relevant in fields beyond Web development, such as devops. I recall 12 years ago a startup tried to recruit me, they were going to revolutionize devops by building automations for Chef. But the metaprogramming in Ruby made it less than ideal for devops work. That startup failed. In most places I now work, a combination of Ansible and Terraform dominate, save where the company uses the constellation of tools surrounding Kubernetes to manage everything. The places that use Chef have been using Chef for 15 years, no new company chooses Chef today.
There was a stretch after 2004 when Ruby was trying to break into new areas. GTK bindings for Ruby were developed and some people experimented with desktop Ruby. But that never took off.
Many of us who loved the metaprogramming in Ruby, but wanted a better structure for that metaprogramming, moved on to other languages such as Clojure.
Some of us loved Ruby because it allowed easier polymorphism than Java did -- after all, you could use higher order functions to produce functions, but without having all the ceremony of building a Factory. But folks who just wanted the polymorphism of functions returning functions eventually moved to NodeJS.
Ruby always felt fluid and magical in a way that made it seem vaguely unsuitable for data science, and eventually Python came to dominate data science.
The Ruby community witnessed the conflict between Rails and Merb, and their eventual union, and eventually Rails became a comfortable framework for quickly generating an API. But for anything that needs high levels of write throughput, no one would chose Ruby, and so other languages have taken some of the market share.
Even on the Web, some big companies (Twitter, AirBnB) either gave up on it completely, or now only use it for templating the frontend, or offering an API to support the frontend. For backend processing, many companies have moved away from it.
In short, Ruby has been losing market share for awhile. In some fields, such as devops, where it used to be competitive, it has almost completely disappeared. It survives only in Web development, and only because of Rails.
None of this can be very inspiring for Ruby developers.