Skip to content

Commit

Permalink
Enable goto def in LSP to goto the actual schema definition (#4434)
Browse files Browse the repository at this point in the history
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: #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 <jordan@jordaneldredge.com>
  • Loading branch information
2 people authored and facebook-github-bot committed Sep 5, 2023
1 parent 5d22d1c commit 3b96b01
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 2 deletions.
68 changes: 67 additions & 1 deletion compiler/crates/relay-bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String>,
}

#[derive(clap::Subcommand)]
Expand Down Expand Up @@ -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<String> {
vec![]
}

fn resolve_field_definition(
&self,
project_name: String,
parent_type: String,
field_info: Option<FieldSchemaInfo>,
) -> Result<Option<FieldDefinitionSourceInfo>, 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::<Vec<_>>();
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::<u64>().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<dyn LSPExtraDataProvider + Send + Sync> =
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<Box<dyn SchemaDocumentationLoader<SDLSchema>>> = None;
let js_language_server = None;

Expand Down
9 changes: 9 additions & 0 deletions vscode-extension/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion vscode-extension/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions vscode-extension/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions vscode-extension/src/languageClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 3b96b01

Please sign in to comment.