From 3b96b01d25f17c535fc0bb7c7ecaf894feaeee49 Mon Sep 17 00:00:00 2001 From: Jon Janzen Date: Tue, 5 Sep 2023 10:10:34 -0700 Subject: [PATCH] Enable goto def in LSP to goto the actual schema definition (#4434) Summary: I basically just took captbaritone 's POC and made it configurable based on our conversation [on Threads](https://www.threads.net/janjonzen/post/CwwfXJ7LlL_) This should be reasonably generalized to support different use-cases, but I'm open to rewriting all of this. Pull Request resolved: https://github.com/facebook/relay/pull/4434 Test Plan: ``` cd compiler cargo build --release --bin relay cd ../vscode-extension yarn build-local ``` In a private project, I configured the new option and overrode the relay binary location: ```diff + "relay.pathToExtraDataProviderScript": "../scripts/graphql_lookup.sh", + "relay.pathToRelay": ".../relay/compiler/target/release/relay" ``` I manually installed the `relay-2.1.0.vsix` file in my VSCode and reloaded I opened a file with a Relay GraphQL query in it and ctrl-clicked (goto def on my machine) and it went to the correct file! Reviewed By: captbaritone Differential Revision: D48955713 Pulled By: bigfootjon fbshipit-source-id: 20f2a707e471c4a8a860711e5625887cf6e4ed70 Co-authored-by: Jordan Eldredge --- compiler/crates/relay-bin/src/main.rs | 68 +++++++++++++++++++++++++- vscode-extension/README.md | 9 ++++ vscode-extension/package.json | 11 ++++- vscode-extension/src/config.ts | 2 + vscode-extension/src/languageClient.ts | 4 ++ 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/compiler/crates/relay-bin/src/main.rs b/compiler/crates/relay-bin/src/main.rs index 8ed2e730c007a..f832244070dd7 100644 --- a/compiler/crates/relay-bin/src/main.rs +++ b/compiler/crates/relay-bin/src/main.rs @@ -29,6 +29,9 @@ use relay_compiler::ProjectName; use relay_compiler::RemotePersister; use relay_lsp::start_language_server; use relay_lsp::DummyExtraDataProvider; +use relay_lsp::FieldDefinitionSourceInfo; +use relay_lsp::FieldSchemaInfo; +use relay_lsp::LSPExtraDataProvider; use schema::SDLSchema; use schema_documentation::SchemaDocumentationLoader; use simplelog::ColorChoice; @@ -108,6 +111,11 @@ struct LspCommand { /// Verbosity level #[clap(long, arg_enum, default_value = "quiet-with-errors")] output: OutputKind, + + /// Script to be called to lookup the actual definition of a GraphQL entity for + /// implementation-first GraphQL schemas. + #[clap(long)] + locate_command: Option, } #[derive(clap::Subcommand)] @@ -306,13 +314,71 @@ async fn handle_compiler_command(command: CompileCommand) -> Result<(), Error> { Ok(()) } +struct ExtraDataProvider { + locate_command: String, +} + +impl ExtraDataProvider { + pub fn new(locate_command: String) -> ExtraDataProvider { + ExtraDataProvider { locate_command } + } +} + +impl LSPExtraDataProvider for ExtraDataProvider { + fn fetch_query_stats(&self, _search_token: &str) -> Vec { + vec![] + } + + fn resolve_field_definition( + &self, + project_name: String, + parent_type: String, + field_info: Option, + ) -> Result, String> { + let entity_name = match field_info { + Some(field_info) => format!("{}.{}", parent_type, field_info.name), + None => parent_type, + }; + let result = Command::new(&self.locate_command) + .arg(project_name) + .arg(entity_name) + .output() + .map_err(|e| format!("Failed to run locate command: {}", e))?; + + let result = String::from_utf8(result.stdout).expect("Failed to parse output"); + + // Parse file_path:line_number:column_number + let result_trimmed = result.trim(); + let result = result_trimmed.split(':').collect::>(); + if result.len() != 3 { + return Err(format!( + "Result '{}' did not match expected format. Please return 'file_path:line_number:column_number'", + result_trimmed + )); + } + let file_path = result[0]; + let line_number = result[1].parse::().unwrap() - 1; + + Ok(Some(FieldDefinitionSourceInfo { + file_path: file_path.to_string(), + line_number, + is_local: true, + })) + } +} + async fn handle_lsp_command(command: LspCommand) -> Result<(), Error> { configure_logger(command.output, TerminalMode::Stderr); let config = get_config(command.config)?; + let extra_data_provider: Box = + match command.locate_command { + Some(locate_command) => Box::new(ExtraDataProvider::new(locate_command)), + None => Box::new(DummyExtraDataProvider::new()), + }; + let perf_logger = Arc::new(ConsoleLogger); - let extra_data_provider = Box::new(DummyExtraDataProvider::new()); let schema_documentation_loader: Option>> = None; let js_language_server = None; diff --git a/vscode-extension/README.md b/vscode-extension/README.md index 554c0a1151035..aac7ec0874f7b 100644 --- a/vscode-extension/README.md +++ b/vscode-extension/README.md @@ -70,6 +70,15 @@ Path to a Relay config relative to the `rootDirectory`. Without this, the compil An array of project configuration in the form `{name: string, rootDirectory: string, pathToConfig: string}`. If omitted, it is assumed your workspace uses a single Relay config and the compiler will search for your config file. But you can also use this configuration if your Relay config is in a nested directory. This configuration must be used if your workspace has multiple Relay projects, each with their own config file. +#### `relay.pathToLocateCommand` (default: `null`) + +Path to a script to look up the actual definition for a GraphQL entity for implementation-first GraphQL schemas. This script will be called for "goto definition" requests to the LSP instead of opening the schema. +The script will be called with 2 arguments. The first will be the relay project name, the second will be either "Type" or "Type.field" (a type or the field of a type, repectively). +The script must respond with a single line of output matching "/absolute/file/path:1:2" where "1" is the line number in the file and "2" is the character on that line that the definition starts with. If it fails +to match this pattern (or the script fails to execute for some reason) the GraphQL schema will be opened as a fallback. + +This option requires >15.0.0 of the Relay compiler to function. + ## Features - IntelliSense diff --git a/vscode-extension/package.json b/vscode-extension/package.json index da13d20ec686c..2fa42528262ed 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -1,7 +1,7 @@ { "name": "relay", "displayName": "Relay GraphQL", - "version": "2.0.0", + "version": "2.1.0", "description": "Relay-powered IDE experience", "repository": { "type": "git", @@ -68,6 +68,15 @@ ], "description": "Controls what is logged to the Output Channel for the Relay language server." }, + "relay.pathToLocateCommand": { + "scope": "workspace", + "default": null, + "type": [ + "string", + "null" + ], + "description": "Path to an optional script to look up the actual definition for a GraphQL entity for implementation-first GraphQL schemas." + }, "relay.pathToRelay": { "scope": "workspace", "default": null, diff --git a/vscode-extension/src/config.ts b/vscode-extension/src/config.ts index 9097f315961d0..2366a650a8a18 100644 --- a/vscode-extension/src/config.ts +++ b/vscode-extension/src/config.ts @@ -11,6 +11,7 @@ export type Config = { rootDirectory: string | null; pathToRelay: string | null; pathToConfig: string | null; + pathToLocateCommand: string | null; lspOutputLevel: string; compilerOutpuLevel: string; autoStartCompiler: boolean; @@ -22,6 +23,7 @@ export function getConfig(scope?: ConfigurationScope): Config { return { pathToRelay: configuration.get('pathToRelay') ?? null, pathToConfig: configuration.get('pathToConfig') ?? null, + pathToLocateCommand: configuration.get('pathToLocateCommand') ?? null, lspOutputLevel: configuration.get('lspOutputLevel') ?? 'quiet-with-errros', compilerOutpuLevel: configuration.get('compilerOutputLevel') ?? 'info', rootDirectory: configuration.get('rootDirectory') ?? null, diff --git a/vscode-extension/src/languageClient.ts b/vscode-extension/src/languageClient.ts index 89028d70dee8a..873b6c46408a2 100644 --- a/vscode-extension/src/languageClient.ts +++ b/vscode-extension/src/languageClient.ts @@ -30,6 +30,10 @@ export function createAndStartLanguageClient(context: RelayExtensionContext) { args.push(config.pathToConfig); } + if (config.pathToLocateCommand) { + args.push(`--locateCommand=${config.pathToLocateCommand}`); + } + const serverOptions: ServerOptions = { options: { cwd: context.relayBinaryExecutionOptions.rootPath,