From ae8010ce25a09ddfc3f03f7c664d1e6104fbcbf2 Mon Sep 17 00:00:00 2001 From: Jon Conley Date: Tue, 12 Aug 2025 18:17:34 -0600 Subject: [PATCH 1/2] Add VS Code rubyLsp.serverPath configuration for local server development... Also add --path option to the ruby-lsp executable itself --- exe/ruby-lsp | 12 +++++++++++ jekyll/contributing.markdown | 14 +++++++++++++ lib/ruby_lsp/setup_bundler.rb | 14 ++++++++++++- test/setup_bundler_test.rb | 32 ++++++++++++++++++++++++++++ vscode/package.json | 5 +++++ vscode/src/client.ts | 39 ++++++++++++++++++++++++++++++----- 6 files changed, 110 insertions(+), 6 deletions(-) diff --git a/exe/ruby-lsp b/exe/ruby-lsp index 3a44d07556..5cced3ea91 100755 --- a/exe/ruby-lsp +++ b/exe/ruby-lsp @@ -29,6 +29,13 @@ parser = OptionParser.new do |opts| options[:branch] = branch end + opts.on( + "--path [PATH]", + "Launch the Ruby LSP using a local PATH rather than the release version", + ) do |path| + options[:path] = path + end + opts.on("--doctor", "Run troubleshooting steps") do options[:doctor] = true end @@ -54,6 +61,11 @@ rescue OptionParser::InvalidOption => e exit(1) end +if options[:branch] && options[:path] + warn('Invalid options: --branch and --path cannot both be set.') + exit(1) +end + # When we're running without bundler, then we need to make sure the composed bundle is fully configured and re-execute # using `BUNDLE_GEMFILE=.ruby-lsp/Gemfile bundle exec ruby-lsp` so that we have access to the gems that are a part of # the application's bundle diff --git a/jekyll/contributing.markdown b/jekyll/contributing.markdown index 270284cdb0..8dc745f720 100644 --- a/jekyll/contributing.markdown +++ b/jekyll/contributing.markdown @@ -58,6 +58,20 @@ with a debugger. Note that the debug mode applies only until the editor is close Caveat: since you are debugging the language server instance that is currently running in your own editor, features will not be available if the execution is currently suspended at a breakpoint. +#### Configuring the server version + +When developing the Ruby LSP server, you may want to test your changes in your own Ruby projects to ensure they work correctly in real-world scenarios. + +The running server, even in debug mode, will default to the installed release version*. You can use the `rubyLsp.serverPath` configuration setting in the target workspace to start your local copy instead. Make sure to restart the language server after making changes to pick up your updates. + +```jsonc +{ + "rubyLsp.serverPath": "/path/to/your/ruby-lsp-clone" +} +``` + +*Note: There are some exceptions to this. Most notably, when the ruby-lsp repository is opened in VS Code with the extension active, it will run the server contained within the repository. + #### Understanding Prism ASTs The Ruby LSP uses Prism to parse and understand Ruby code. When working on a feature, it's very common to need to diff --git a/lib/ruby_lsp/setup_bundler.rb b/lib/ruby_lsp/setup_bundler.rb index e520dd1498..f05ede66f6 100644 --- a/lib/ruby_lsp/setup_bundler.rb +++ b/lib/ruby_lsp/setup_bundler.rb @@ -35,7 +35,13 @@ def stdout def initialize(project_path, **options) @project_path = project_path @branch = options[:branch] #: String? + @path = options[:path] #: String? @launcher = options[:launcher] #: bool? + + if @branch && !@branch.empty? && @path && !@path.empty? + raise ArgumentError, "Branch and path options are mutually exclusive. Please specify only one." + end + patch_thor_to_print_progress_to_stderr! if @launcher # Regular bundle paths @@ -165,7 +171,13 @@ def write_custom_gemfile unless @dependencies["ruby-lsp"] ruby_lsp_entry = +'gem "ruby-lsp", require: false, group: :development' - ruby_lsp_entry << ", github: \"Shopify/ruby-lsp\", branch: \"#{@branch}\"" if @branch + if @branch && !@branch.empty? + ruby_lsp_entry << ", github: \"Shopify/ruby-lsp\", branch: \"#{@branch}\"" + end + if @path && !@path.empty? + absolute_path = File.expand_path(@path, @project_path) + ruby_lsp_entry << ", path: \"#{absolute_path}\"" + end parts << ruby_lsp_entry end diff --git a/test/setup_bundler_test.rb b/test/setup_bundler_test.rb index ca504d5307..4542bbe283 100644 --- a/test/setup_bundler_test.rb +++ b/test/setup_bundler_test.rb @@ -280,6 +280,38 @@ def test_creates_composed_bundle_with_specified_branch end end + def test_creates_composed_bundle_with_specified_path + Dir.mktmpdir do |dir| + local_path = "local-ruby-lsp" + FileUtils.mkdir_p(File.join(dir, local_path, "lib")) + + Dir.chdir(dir) do + bundle_gemfile = Pathname.new(".ruby-lsp").expand_path(Dir.pwd) + "Gemfile" + Bundler.with_unbundled_env do + stub_bundle_with_env(bundle_env(dir, bundle_gemfile.to_s)) + run_script(File.realpath(dir), path: local_path) + end + + assert_path_exists(".ruby-lsp") + assert_path_exists(".ruby-lsp/Gemfile") + expected_absolute_path = File.expand_path(local_path, File.realpath(dir)) + assert_match(%r{ruby-lsp.*path: "#{Regexp.escape(expected_absolute_path)}"}, File.read(".ruby-lsp/Gemfile")) + assert_match("debug", File.read(".ruby-lsp/Gemfile")) + end + end + end + + def test_raises_error_when_both_branch_and_path_are_specified + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + error = assert_raises(ArgumentError) do + RubyLsp::SetupBundler.new(dir, branch: "test-branch", path: "local-path") + end + assert_equal("Branch and path options are mutually exclusive. Please specify only one.", error.message) + end + end + end + def test_returns_bundle_app_config_if_there_is_local_config Dir.mktmpdir do |dir| Dir.chdir(dir) do diff --git a/vscode/package.json b/vscode/package.json index fac2b8ed93..161b11439c 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -467,6 +467,11 @@ "type": "string", "default": "" }, + "rubyLsp.serverPath": { + "description": "Absolute or workspace-relative path to a local ruby-lsp repository or its ruby-lsp executable. Only supported if not using bundleGemfile", + "type": "string", + "default": "" + }, "rubyLsp.pullDiagnosticsOn": { "description": "When to pull diagnostics from the server (on change, save or both). Selecting 'save' may significantly improve performance on large files", "type": "string", diff --git a/vscode/src/client.ts b/vscode/src/client.ts index d7cf40784d..3e6c487aa4 100644 --- a/vscode/src/client.ts +++ b/vscode/src/client.ts @@ -1,4 +1,5 @@ import path from "path"; +import fs from "fs"; import os from "os"; import { performance as Perf } from "perf_hooks"; @@ -85,6 +86,7 @@ function getLspExecutables(workspaceFolder: vscode.WorkspaceFolder, env: NodeJS. const customBundleGemfile: string = config.get("bundleGemfile")!; const useBundlerCompose: boolean = config.get("useBundlerCompose")!; const bypassTypechecker: boolean = config.get("bypassTypechecker")!; + const serverPath: string = config.get("serverPath")!; const executableOptions: ExecutableOptions = { cwd: workspaceFolder.uri.fsPath, @@ -119,13 +121,40 @@ function getLspExecutables(workspaceFolder: vscode.WorkspaceFolder, env: NodeJS. options: executableOptions, }; } else { + const args = []; const workspacePath = workspaceFolder.uri.fsPath; - const command = - path.basename(workspacePath) === "ruby-lsp" && os.platform() !== "win32" - ? path.join(workspacePath, "exe", "ruby-lsp") - : "ruby-lsp"; + let command: string; - const args = []; + if (serverPath.length > 0 && branch.length > 0) { + throw new Error( + 'Invalid configuration: "rubyLsp.serverPath" and "rubyLsp.branch" cannot both be set. Please unset one of them.', + ); + } + + if (serverPath.length > 0) { + const absoluteServerPath = path.isAbsolute(serverPath) ? serverPath : path.resolve(workspacePath, serverPath); + const exists = fs.existsSync(absoluteServerPath); + + if (exists) { + args.push("--path", absoluteServerPath); + const stat = fs.statSync(absoluteServerPath); + + if (stat.isDirectory()) { + command = os.platform() !== "win32" ? path.join(absoluteServerPath, "exe", "ruby-lsp") : "ruby-lsp"; + } else { + command = absoluteServerPath; + } + } else { + throw new Error( + `The configured rubyLsp.serverPath "${serverPath}" does not exist at "${absoluteServerPath}". `, + ); + } + } else { + command = + path.basename(workspacePath) === "ruby-lsp" && os.platform() !== "win32" + ? path.join(workspacePath, "exe", "ruby-lsp") + : "ruby-lsp"; + } if (branch.length > 0) { args.push("--branch", branch); From 03237a0635c64a80173065041b4bfe32e3643b4a Mon Sep 17 00:00:00 2001 From: Jon Conley Date: Mon, 18 Aug 2025 21:33:27 -0600 Subject: [PATCH 2/2] Rename --path to --lsp-path and require repo absolute path - Rename CLI flag from --path to --lsp-path to prevent confusion with other path configurations - Add validation in ruby-lsp executable to require absolute paths to repo root for --lsp-path option - Do not allow serverPath to directly be an executable - Remove incorrect path expansion in SetupBundler (LSP path and project path are unrelated) - Simplify VS Code extension to pass serverPath directly without validation or fs usage - Remove duplicate validation between executable and SetupBundler - Update package.json to specify absolute path requirement and remove default value - Remove manual restart instruction from documentation (server auto-restarts on config changes) - Fix potential runtime error when serverPath is undefined by using truthy check --- exe/ruby-lsp | 13 ++++++---- jekyll/contributing.markdown | 2 +- lib/ruby_lsp/setup_bundler.rb | 16 +++---------- test/setup_bundler_test.rb | 19 ++++----------- vscode/package.json | 5 ++-- vscode/src/client.ts | 45 ++++++++--------------------------- 6 files changed, 29 insertions(+), 71 deletions(-) diff --git a/exe/ruby-lsp b/exe/ruby-lsp index 5cced3ea91..574caeb39f 100755 --- a/exe/ruby-lsp +++ b/exe/ruby-lsp @@ -30,10 +30,10 @@ parser = OptionParser.new do |opts| end opts.on( - "--path [PATH]", + "--lsp-path [PATH]", "Launch the Ruby LSP using a local PATH rather than the release version", ) do |path| - options[:path] = path + options[:lsp_path] = path end opts.on("--doctor", "Run troubleshooting steps") do @@ -61,8 +61,13 @@ rescue OptionParser::InvalidOption => e exit(1) end -if options[:branch] && options[:path] - warn('Invalid options: --branch and --path cannot both be set.') +if options[:branch] && options[:lsp_path] + warn('Invalid options: --branch and --lsp-path cannot both be set.') + exit(1) +end + +if options[:lsp_path] && !File.absolute_path?(options[:lsp_path]) + warn('Invalid option: --lsp-path must be an absolute path.') exit(1) end diff --git a/jekyll/contributing.markdown b/jekyll/contributing.markdown index 8dc745f720..f48452446a 100644 --- a/jekyll/contributing.markdown +++ b/jekyll/contributing.markdown @@ -62,7 +62,7 @@ not be available if the execution is currently suspended at a breakpoint. When developing the Ruby LSP server, you may want to test your changes in your own Ruby projects to ensure they work correctly in real-world scenarios. -The running server, even in debug mode, will default to the installed release version*. You can use the `rubyLsp.serverPath` configuration setting in the target workspace to start your local copy instead. Make sure to restart the language server after making changes to pick up your updates. +The running server, even in debug mode, will default to the installed release version*. You can use the `rubyLsp.serverPath` configuration setting in the target workspace to start your local copy instead. ```jsonc { diff --git a/lib/ruby_lsp/setup_bundler.rb b/lib/ruby_lsp/setup_bundler.rb index f05ede66f6..c614e65bac 100644 --- a/lib/ruby_lsp/setup_bundler.rb +++ b/lib/ruby_lsp/setup_bundler.rb @@ -35,13 +35,8 @@ def stdout def initialize(project_path, **options) @project_path = project_path @branch = options[:branch] #: String? - @path = options[:path] #: String? + @lsp_path = options[:lsp_path] #: String? @launcher = options[:launcher] #: bool? - - if @branch && !@branch.empty? && @path && !@path.empty? - raise ArgumentError, "Branch and path options are mutually exclusive. Please specify only one." - end - patch_thor_to_print_progress_to_stderr! if @launcher # Regular bundle paths @@ -171,13 +166,8 @@ def write_custom_gemfile unless @dependencies["ruby-lsp"] ruby_lsp_entry = +'gem "ruby-lsp", require: false, group: :development' - if @branch && !@branch.empty? - ruby_lsp_entry << ", github: \"Shopify/ruby-lsp\", branch: \"#{@branch}\"" - end - if @path && !@path.empty? - absolute_path = File.expand_path(@path, @project_path) - ruby_lsp_entry << ", path: \"#{absolute_path}\"" - end + ruby_lsp_entry << ", github: \"Shopify/ruby-lsp\", branch: \"#{@branch}\"" if @branch + ruby_lsp_entry << ", path: \"#{@lsp_path}\"" if @lsp_path parts << ruby_lsp_entry end diff --git a/test/setup_bundler_test.rb b/test/setup_bundler_test.rb index 4542bbe283..f93c3d1091 100644 --- a/test/setup_bundler_test.rb +++ b/test/setup_bundler_test.rb @@ -282,35 +282,24 @@ def test_creates_composed_bundle_with_specified_branch def test_creates_composed_bundle_with_specified_path Dir.mktmpdir do |dir| - local_path = "local-ruby-lsp" - FileUtils.mkdir_p(File.join(dir, local_path, "lib")) + local_path = File.join(dir, "local-ruby-lsp") + FileUtils.mkdir_p(File.join(local_path, "lib")) Dir.chdir(dir) do bundle_gemfile = Pathname.new(".ruby-lsp").expand_path(Dir.pwd) + "Gemfile" Bundler.with_unbundled_env do stub_bundle_with_env(bundle_env(dir, bundle_gemfile.to_s)) - run_script(File.realpath(dir), path: local_path) + run_script(File.realpath(dir), lsp_path: local_path) end assert_path_exists(".ruby-lsp") assert_path_exists(".ruby-lsp/Gemfile") - expected_absolute_path = File.expand_path(local_path, File.realpath(dir)) - assert_match(%r{ruby-lsp.*path: "#{Regexp.escape(expected_absolute_path)}"}, File.read(".ruby-lsp/Gemfile")) + assert_match(%r{ruby-lsp.*path: "#{Regexp.escape(local_path)}"}, File.read(".ruby-lsp/Gemfile")) assert_match("debug", File.read(".ruby-lsp/Gemfile")) end end end - def test_raises_error_when_both_branch_and_path_are_specified - Dir.mktmpdir do |dir| - Dir.chdir(dir) do - error = assert_raises(ArgumentError) do - RubyLsp::SetupBundler.new(dir, branch: "test-branch", path: "local-path") - end - assert_equal("Branch and path options are mutually exclusive. Please specify only one.", error.message) - end - end - end def test_returns_bundle_app_config_if_there_is_local_config Dir.mktmpdir do |dir| diff --git a/vscode/package.json b/vscode/package.json index 161b11439c..a360174179 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -468,9 +468,8 @@ "default": "" }, "rubyLsp.serverPath": { - "description": "Absolute or workspace-relative path to a local ruby-lsp repository or its ruby-lsp executable. Only supported if not using bundleGemfile", - "type": "string", - "default": "" + "description": "Absolute path to a local ruby-lsp repository. Only supported if not using bundleGemfile", + "type": "string" }, "rubyLsp.pullDiagnosticsOn": { "description": "When to pull diagnostics from the server (on change, save or both). Selecting 'save' may significantly improve performance on large files", diff --git a/vscode/src/client.ts b/vscode/src/client.ts index 3e6c487aa4..4ae716eec7 100644 --- a/vscode/src/client.ts +++ b/vscode/src/client.ts @@ -1,5 +1,4 @@ import path from "path"; -import fs from "fs"; import os from "os"; import { performance as Perf } from "perf_hooks"; @@ -121,45 +120,21 @@ function getLspExecutables(workspaceFolder: vscode.WorkspaceFolder, env: NodeJS. options: executableOptions, }; } else { - const args = []; - const workspacePath = workspaceFolder.uri.fsPath; - let command: string; - - if (serverPath.length > 0 && branch.length > 0) { - throw new Error( - 'Invalid configuration: "rubyLsp.serverPath" and "rubyLsp.branch" cannot both be set. Please unset one of them.', - ); - } - - if (serverPath.length > 0) { - const absoluteServerPath = path.isAbsolute(serverPath) ? serverPath : path.resolve(workspacePath, serverPath); - const exists = fs.existsSync(absoluteServerPath); - - if (exists) { - args.push("--path", absoluteServerPath); - const stat = fs.statSync(absoluteServerPath); - - if (stat.isDirectory()) { - command = os.platform() !== "win32" ? path.join(absoluteServerPath, "exe", "ruby-lsp") : "ruby-lsp"; - } else { - command = absoluteServerPath; - } - } else { - throw new Error( - `The configured rubyLsp.serverPath "${serverPath}" does not exist at "${absoluteServerPath}". `, - ); - } - } else { - command = - path.basename(workspacePath) === "ruby-lsp" && os.platform() !== "win32" - ? path.join(workspacePath, "exe", "ruby-lsp") - : "ruby-lsp"; - } + const basePath = serverPath || workspaceFolder.uri.fsPath; + const command = + path.basename(basePath) === "ruby-lsp" && os.platform() !== "win32" + ? path.join(basePath, "exe", "ruby-lsp") + : "ruby-lsp"; + const args = []; if (branch.length > 0) { args.push("--branch", branch); } + if (serverPath) { + args.push("--lsp-path", serverPath); + } + if (featureEnabled("launcher")) { args.push("--use-launcher"); }