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 docker compose as a version manager #2919

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions exe/ruby-lsp
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ parser = OptionParser.new do |opts|
options[:launcher] = true
end

opts.on("--local-fs-map [FS_MAP]", "Local fs map in format 'local:remote,local2:remote2,...'") do |map|
options[:local_fs_map] = map
end

opts.on("-h", "--help", "Print this help") do
puts opts.help
puts
Expand Down Expand Up @@ -146,6 +150,13 @@ if options[:doctor]
return
end

if options[:local_fs_map]
ENV["RUBY_LSP_LOCAL_FS_MAP"] = [
ENV["RUBY_LSP_LOCAL_FS_MAP"],
options[:local_fs_map],
].reject(&:nil?).join(",")
end

# Ensure all output goes out stderr by default to allow puts/p/pp to work
# without specifying output device.
$> = $stderr
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_lsp/base_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def start
if uri
begin
parsed_uri = URI(uri)
parsed_uri = @global_state.to_internal_uri(parsed_uri)
message[:params][:textDocument][:uri] = parsed_uri

# We don't want to try to parse documents on text synchronization notifications
Expand Down
54 changes: 53 additions & 1 deletion lib/ruby_lsp/global_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ class GlobalState
sig { returns(ClientCapabilities) }
attr_reader :client_capabilities

sig { returns(T::Hash[String, String]) }
attr_accessor :local_fs_map

sig { void }
def initialize
@workspace_uri = T.let(URI::Generic.from_path(path: Dir.pwd), URI::Generic)
Expand All @@ -53,6 +56,7 @@ def initialize
)
@client_capabilities = T.let(ClientCapabilities.new, ClientCapabilities)
@enabled_feature_flags = T.let({}, T::Hash[Symbol, T::Boolean])
@local_fs_map = T.let(build_local_fs_map_from_env, T::Hash[String, String])
end

sig { params(addon_name: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
Expand Down Expand Up @@ -81,8 +85,15 @@ def apply_options(options)
notifications = []
direct_dependencies = gather_direct_dependencies
all_dependencies = gather_direct_and_indirect_dependencies

options.dig(:initializationOptions, :localFsMap)&.each do |local, remote|
local_fs_map[local.to_s] = remote
end

workspace_uri = options.dig(:workspaceFolders, 0, :uri)
@workspace_uri = URI(workspace_uri) if workspace_uri
if workspace_uri
@workspace_uri = to_internal_uri(URI(workspace_uri))
end

specified_formatter = options.dig(:initializationOptions, :formatter)

Expand Down Expand Up @@ -171,6 +182,36 @@ def supports_watching_files
@client_capabilities.supports_watching_files
end

sig { params(uri: URI::Generic).returns(URI::Generic) }
def to_internal_uri(uri)
path = uri.path
return uri unless path

local_fs_map.each do |external, internal|
next unless path.start_with?(external)

uri.path = path.sub(external, internal)
return uri
end

uri
end

sig { params(uri: URI::Generic).returns(URI::Generic) }
def to_external_uri(uri)
path = uri.path
return uri unless path

local_fs_map.each do |external, internal|
next unless path.start_with?(internal)

uri.path = path.sub(internal, external)
return uri
end

uri
end

private

sig { params(direct_dependencies: T::Array[String], all_dependencies: T::Array[String]).returns(String) }
Expand Down Expand Up @@ -263,5 +304,16 @@ def gather_direct_and_indirect_dependencies
rescue Bundler::GemfileNotFound
[]
end

sig { returns(T::Hash[String, String]) }
def build_local_fs_map_from_env
env = ENV["RUBY_LSP_LOCAL_FS_MAP"]
return {} unless env

env.split(",").each_with_object({}) do |pair, map|
local, remote = pair.split(":", 2)
map[local] = remote
end
end
end
end
26 changes: 20 additions & 6 deletions lib/ruby_lsp/listeners/definition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,10 @@ def handle_global_variable_definition(name)
entries.each do |entry|
location = entry.location

external_uri = @global_state.to_external_uri(entry.uri)

@response_builder << Interface::Location.new(
uri: entry.uri.to_s,
uri: external_uri.to_s,
range: Interface::Range.new(
start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
Expand All @@ -247,8 +249,10 @@ def handle_instance_variable_definition(name)
entries.each do |entry|
location = entry.location

external_uri = @global_state.to_external_uri(entry.uri)

@response_builder << Interface::Location.new(
uri: entry.uri.to_s,
uri: external_uri.to_s,
range: Interface::Range.new(
start: Interface::Position.new(line: location.start_line - 1, character: location.start_column),
end: Interface::Position.new(line: location.end_line - 1, character: location.end_column),
Expand Down Expand Up @@ -278,8 +282,10 @@ def handle_method_definition(message, receiver_type, inherited_only: false)
uri = target_method.uri
next if sorbet_level_true_or_higher?(@sorbet_level) && not_in_dependencies?(T.must(uri.full_path))

external_uri = @global_state.to_external_uri(uri)

@response_builder << Interface::LocationLink.new(
target_uri: uri.to_s,
target_uri: external_uri.to_s,
target_range: range_from_location(target_method.location),
target_selection_range: range_from_location(target_method.name_location),
)
Expand All @@ -298,8 +304,11 @@ def handle_require_definition(node, message)
candidate = entry.full_path

if candidate
uri = URI::Generic.from_path(path: candidate)
external_uri = @global_state.to_external_uri(uri)

@response_builder << Interface::Location.new(
uri: URI::Generic.from_path(path: candidate).to_s,
uri: external_uri.to_s,
range: Interface::Range.new(
start: Interface::Position.new(line: 0, character: 0),
end: Interface::Position.new(line: 0, character: 0),
Expand All @@ -313,8 +322,11 @@ def handle_require_definition(node, message)
current_folder = path ? Pathname.new(CGI.unescape(path)).dirname : @global_state.workspace_path
candidate = File.expand_path(File.join(current_folder, required_file))

uri = URI::Generic.from_path(path: candidate)
external_uri = @global_state.to_external_uri(uri)

@response_builder << Interface::Location.new(
uri: URI::Generic.from_path(path: candidate).to_s,
uri: external_uri.to_s,
range: Interface::Range.new(
start: Interface::Position.new(line: 0, character: 0),
end: Interface::Position.new(line: 0, character: 0),
Expand Down Expand Up @@ -351,8 +363,10 @@ def find_in_index(value)
uri = entry.uri
next if @sorbet_level != RubyDocument::SorbetLevel::Ignore && not_in_dependencies?(T.must(uri.full_path))

external_uri = @global_state.to_external_uri(uri)

@response_builder << Interface::LocationLink.new(
target_uri: uri.to_s,
target_uri: external_uri.to_s,
target_range: range_from_location(entry.location),
target_selection_range: range_from_location(entry.name_location),
)
Expand Down
4 changes: 3 additions & 1 deletion lib/ruby_lsp/requests/references.rb
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,10 @@ def collect_references(target, parse_result, uri)
dispatcher.visit(parse_result.value)

finder.references.each do |reference|
external_uri = @global_state.to_external_uri(uri)

@locations << Interface::Location.new(
uri: uri.to_s,
uri: external_uri.to_s,
range: range_from_location(reference.location),
)
end
Expand Down
5 changes: 4 additions & 1 deletion lib/ruby_lsp/requests/support/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,14 @@ def categorized_markdown_from_index_entries(title, entries, max_entries = nil)
entries_to_format.each do |entry|
loc = entry.location

@global_state ||= T.let(GlobalState.new, T.nilable(RubyLsp::GlobalState))
external_uri = @global_state.to_external_uri(entry.uri)

# We always handle locations as zero based. However, for file links in Markdown we need them to be one
# based, which is why instead of the usual subtraction of 1 to line numbers, we are actually adding 1 to
# columns. The format for VS Code file URIs is
# `file:///path/to/file.rb#Lstart_line,start_column-end_line,end_column`
uri = "#{entry.uri}#L#{loc.start_line},#{loc.start_column + 1}-#{loc.end_line},#{loc.end_column + 1}"
uri = "#{external_uri}#L#{loc.start_line},#{loc.start_column + 1}-#{loc.end_line},#{loc.end_column + 1}"
definitions << "[#{entry.file_name}](#{uri})"
content << "\n\n#{entry.comments}" unless entry.comments.empty?
end
Expand Down
4 changes: 3 additions & 1 deletion lib/ruby_lsp/requests/workspace_symbol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,14 @@ def perform
# short name `Bar`, then searching for `Foo::Bar` would not return any results
*container, _short_name = entry.name.split("::")

external_uri = @global_state.to_external_uri(uri)

Interface::WorkspaceSymbol.new(
name: entry.name,
container_name: container.join("::"),
kind: kind,
location: Interface::Location.new(
uri: uri.to_s,
uri: external_uri.to_s,
range: Interface::Range.new(
start: Interface::Position.new(line: loc.start_line - 1, character: loc.start_column),
end: Interface::Position.new(line: loc.end_line - 1, character: loc.end_column),
Expand Down
7 changes: 6 additions & 1 deletion lib/ruby_lsp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,8 @@ def text_document_did_close(message)
uri = message.dig(:params, :textDocument, :uri)
@store.delete(uri)

uri = global_state.to_external_uri(uri)

# Clear diagnostics for the closed file, so that they no longer appear in the problems tab
send_message(
Notification.new(
Expand Down Expand Up @@ -1106,10 +1108,13 @@ def workspace_dependencies(message)
dep_keys = definition.locked_deps.keys.to_set

definition.specs.map do |spec|
uri = URI("file://#{spec.full_gem_path}")
uri = global_state.to_external_uri(uri)

{
name: spec.name,
version: spec.version,
path: spec.full_gem_path,
path: uri.path,
dependency: dep_keys.include?(spec.name),
}
end
Expand Down
9 changes: 9 additions & 0 deletions vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@
"rvm",
"shadowenv",
"mise",
"compose",
"custom"
],
"default": "auto"
Expand All @@ -357,6 +358,14 @@
"chrubyRubies": {
"description": "An array of extra directories to search for Ruby installations when using chruby. Equivalent to the RUBIES environment variable",
"type": "array"
},
"composeService": {
"description": "The name of the service in the compose file to use to start the Ruby LSP server",
"type": "string"
},
"composeCustomCommand": {
"description": "A shell command to start the ruby LSP server using compose",
"type": "string"
}
},
"default": {
Expand Down
13 changes: 9 additions & 4 deletions vscode/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function enabledFeatureFlags(): Record<string, boolean> {
// Get the executables to start the server based on the user's configuration
function getLspExecutables(
workspaceFolder: vscode.WorkspaceFolder,
env: NodeJS.ProcessEnv,
ruby: Ruby,
): ServerOptions {
let run: Executable;
let debug: Executable;
Expand All @@ -73,8 +73,8 @@ function getLspExecutables(
const executableOptions: ExecutableOptions = {
cwd: workspaceFolder.uri.fsPath,
env: bypassTypechecker
? { ...env, RUBY_LSP_BYPASS_TYPECHECKER: "true" }
: env,
? { ...ruby.env, RUBY_LSP_BYPASS_TYPECHECKER: "true" }
: ruby.env,
shell: true,
};

Expand Down Expand Up @@ -128,6 +128,9 @@ function getLspExecutables(
};
}

run = ruby.activateExecutable(run);
debug = ruby.activateExecutable(debug);

return { run, debug };
}

Expand All @@ -152,6 +155,7 @@ function collectClientOptions(
const supportedSchemes = ["file", "git"];

const fsPath = workspaceFolder.uri.fsPath.replace(/\/$/, "");
const pathMapping = ruby.pathMapping;

// For each workspace, the language client is responsible for handling requests for:
// 1. Files inside of the workspace itself
Expand Down Expand Up @@ -227,6 +231,7 @@ function collectClientOptions(
indexing: configuration.get("indexing"),
addonSettings: configuration.get("addonSettings"),
enabledFeatureFlags: enabledFeatureFlags(),
localFsMap: pathMapping,
},
};
}
Expand Down Expand Up @@ -333,7 +338,7 @@ export default class Client extends LanguageClient implements ClientInterface {
) {
super(
LSP_NAME,
getLspExecutables(workspaceFolder, ruby.env),
getLspExecutables(workspaceFolder, ruby),
collectClientOptions(
vscode.workspace.getConfiguration("rubyLsp"),
workspaceFolder,
Expand Down
31 changes: 30 additions & 1 deletion vscode/src/common.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { exec } from "child_process";
import { exec, spawn as originalSpawn } from "child_process";
import { createHash } from "crypto";
import { promisify } from "util";

import * as vscode from "vscode";
import { State } from "vscode-languageclient";
import { Executable } from "vscode-languageclient/node";

export enum Command {
Start = "rubyLsp.start",
Expand Down Expand Up @@ -70,6 +71,8 @@ export const STATUS_EMITTER = new vscode.EventEmitter<
WorkspaceInterface | undefined
>();

export const spawn = originalSpawn;

export const asyncExec = promisify(exec);
export const LSP_NAME = "Ruby LSP";
export const LOG_CHANNEL = vscode.window.createOutputChannel(LSP_NAME, {
Expand Down Expand Up @@ -146,3 +149,29 @@ export function featureEnabled(feature: keyof typeof FEATURE_FLAGS): boolean {
// If that number is below the percentage, then the feature is enabled for this user
return hashNum < percentage;
}

export function parseCommand(commandString: string): Executable {
// Regular expression to split arguments while respecting quotes
const regex = /(?:[^\s"']+|"[^"]*"|'[^']*')+/g;

const parts =
commandString.match(regex)?.map((arg) => {
// Remove surrounding quotes, if any
return arg.replace(/^['"]|['"]$/g, "");
}) ?? [];

// Extract environment variables
const env: Record<string, string> = {};
while (parts[0] && parts[0].includes("=")) {
const [key, value] = parts.shift()?.split("=") ?? [];
if (key) {
env[key] = value || "";
}
}

// The first part is the command, the rest are arguments
const command = parts.shift() || "";
const args = parts;

return { command, args, options: { env } };
}
Loading
Loading