diff --git a/compiler/crates/relay-lsp/src/graphql_tools.rs b/compiler/crates/relay-lsp/src/graphql_tools.rs index e74fcb8a13c57..f8e27bd2477fe 100644 --- a/compiler/crates/relay-lsp/src/graphql_tools.rs +++ b/compiler/crates/relay-lsp/src/graphql_tools.rs @@ -79,7 +79,7 @@ impl Request for GraphQLExecuteQuery { /// This function will return the program that contains only operation /// and all referenced fragments. /// We can use it to print the full query text -fn get_operation_only_program( +pub fn get_operation_only_program( operation: Arc, fragments: Vec>, program: &Program, diff --git a/compiler/crates/relay-lsp/src/lib.rs b/compiler/crates/relay-lsp/src/lib.rs index 5352993c3bddb..511f56b5bea3d 100644 --- a/compiler/crates/relay-lsp/src/lib.rs +++ b/compiler/crates/relay-lsp/src/lib.rs @@ -23,6 +23,7 @@ mod lsp_extra_data_provider; pub mod lsp_process_error; pub mod lsp_runtime_error; pub mod node_resolution_info; +pub mod print_operation; pub mod references; pub mod rename; mod resolved_types_at_location; diff --git a/compiler/crates/relay-lsp/src/print_operation.rs b/compiler/crates/relay-lsp/src/print_operation.rs new file mode 100644 index 0000000000000..7ccad74ac0f19 --- /dev/null +++ b/compiler/crates/relay-lsp/src/print_operation.rs @@ -0,0 +1,92 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +use graphql_ir::OperationDefinitionName; +use lsp_types::request::Request; +use lsp_types::TextDocumentPositionParams; +use serde::Deserialize; +use serde::Serialize; + +use crate::GlobalState; +use crate::LSPRuntimeError; +use crate::LSPRuntimeResult; + +pub(crate) fn on_print_operation( + state: &impl GlobalState, + params: ::Params, +) -> LSPRuntimeResult<::Result> { + let text_document_uri = params + .text_document_position_params + .text_document + .uri + .clone(); + + let project_name = state.extract_project_name_from_url(&text_document_uri)?; + let executable_document_under_cursor = + state.extract_executable_document_from_text(¶ms.text_document_position_params, 1); + + let operation_name = match executable_document_under_cursor { + Ok((document, _)) => { + get_first_operation_name(&document.definitions).ok_or(LSPRuntimeError::ExpectedError) + } + Err(_) => { + let executable_definitions = + state.resolve_executable_definitions(&text_document_uri)?; + + if executable_definitions.is_empty() { + return Err(LSPRuntimeError::ExpectedError); + } + + get_first_operation_name(&executable_definitions).ok_or(LSPRuntimeError::ExpectedError) + } + }?; + + state + .get_operation_text(operation_name, &project_name) + .map(|operation_text| PrintOperationResponse { + operation_name: operation_name.0.to_string(), + operation_text, + }) +} + +fn get_first_operation_name( + executable_definitions: &[graphql_syntax::ExecutableDefinition], +) -> Option { + executable_definitions.iter().find_map(|definition| { + if let graphql_syntax::ExecutableDefinition::Operation(operation) = definition { + if let Some(name) = &operation.name { + return Some(OperationDefinitionName(name.value)); + } + + None + } else { + None + } + }) +} + +pub(crate) enum PrintOperation {} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PrintOperationParams { + #[serde(flatten)] + pub text_document_position_params: TextDocumentPositionParams, +} + +#[derive(Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub(crate) struct PrintOperationResponse { + pub operation_name: String, + pub operation_text: String, +} + +impl Request for PrintOperation { + type Params = PrintOperationParams; + type Result = PrintOperationResponse; + const METHOD: &'static str = "relay/printOperation"; +} diff --git a/compiler/crates/relay-lsp/src/server.rs b/compiler/crates/relay-lsp/src/server.rs index 590393c0be9fd..416d1b5a0546c 100644 --- a/compiler/crates/relay-lsp/src/server.rs +++ b/compiler/crates/relay-lsp/src/server.rs @@ -80,6 +80,8 @@ use crate::hover::on_hover; use crate::inlay_hints::on_inlay_hint_request; use crate::lsp_process_error::LSPProcessResult; use crate::lsp_runtime_error::LSPRuntimeError; +use crate::print_operation::on_print_operation; +use crate::print_operation::PrintOperation; use crate::references::on_references; use crate::rename::on_prepare_rename; use crate::rename::on_rename; @@ -258,6 +260,7 @@ fn dispatch_request(request: lsp_server::Request, lsp_state: &impl GlobalState) .on_request_sync::( on_get_source_location_of_type_definition, )? + .on_request_sync::(on_print_operation)? .on_request_sync::(on_hover)? .on_request_sync::(on_goto_definition)? .on_request_sync::(on_references)? diff --git a/compiler/crates/relay-lsp/src/server/lsp_state.rs b/compiler/crates/relay-lsp/src/server/lsp_state.rs index b03b3d929fd3a..1d3534ea78c02 100644 --- a/compiler/crates/relay-lsp/src/server/lsp_state.rs +++ b/compiler/crates/relay-lsp/src/server/lsp_state.rs @@ -23,12 +23,14 @@ use fnv::FnvBuildHasher; use graphql_ir::build_ir_with_extra_features; use graphql_ir::BuilderOptions; use graphql_ir::FragmentVariablesSemantic; +use graphql_ir::OperationDefinitionName; use graphql_ir::Program; use graphql_ir::RelayMode; use graphql_syntax::parse_executable_with_error_recovery_and_parser_features; use graphql_syntax::ExecutableDefinition; use graphql_syntax::ExecutableDocument; use graphql_syntax::GraphQLSource; +use graphql_text_printer::print_full_operation; use intern::string_key::Intern; use intern::string_key::StringKey; use log::debug; @@ -44,6 +46,7 @@ use relay_compiler::FileGroup; use relay_compiler::ProjectName; use relay_docblock::parse_docblock_ast; use relay_docblock::ParseOptions; +use relay_transforms::apply_transforms; use relay_transforms::deprecated_fields_for_executable_definition; use schema::SDLSchema; use schema_documentation::CombinedSchemaDocumentation; @@ -54,6 +57,7 @@ use tokio::sync::Notify; use super::task_queue::TaskScheduler; use crate::diagnostic_reporter::DiagnosticReporter; use crate::docblock_resolution_info::create_docblock_resolution_info; +use crate::graphql_tools::get_operation_only_program; use crate::graphql_tools::get_query_text; use crate::location::transform_relay_location_to_lsp_location_with_cache; use crate::lsp_runtime_error::LSPRuntimeResult; @@ -129,6 +133,12 @@ pub trait GlobalState { project_name: &StringKey, ) -> LSPRuntimeResult; + fn get_operation_text( + &self, + operation_name: OperationDefinitionName, + project_name: &StringKey, + ) -> LSPRuntimeResult; + fn document_opened(&self, url: &Url, text: &str) -> LSPRuntimeResult<()>; fn document_changed(&self, url: &Url, text: &str) -> LSPRuntimeResult<()>; @@ -575,6 +585,55 @@ impl LSPRuntimeResult { + let project_config = self + .config + .enabled_projects() + .find(|project_config| project_config.name == (*project_name).into()) + .ok_or_else(|| { + LSPRuntimeError::UnexpectedError(format!( + "Unable to get project config for project {}.", + project_name + )) + })?; + + let program = self.get_program(project_name)?; + + let operation_only_program = program + .operation(operation_name) + .and_then(|operation| { + get_operation_only_program(Arc::clone(operation), vec![], &program) + }) + .ok_or(LSPRuntimeError::ExpectedError)?; + + let programs = apply_transforms( + project_config, + Arc::new(operation_only_program), + Default::default(), + Arc::clone(&self.perf_logger), + None, + self.config.custom_transforms.as_ref(), + ) + .map_err(|_| LSPRuntimeError::ExpectedError)?; + + let operation_to_print = programs + .operation_text + .operation(operation_name) + .ok_or(LSPRuntimeError::ExpectedError)?; + + let operation_text = print_full_operation( + &programs.operation_text, + operation_to_print, + Default::default(), + ); + + Ok(operation_text) + } + fn document_opened(&self, uri: &Url, text: &str) -> LSPRuntimeResult<()> { let file_group = get_file_group_from_uri(&self.file_categorizer, uri, &self.root_dir, &self.config)?; diff --git a/vscode-extension/package.json b/vscode-extension/package.json index a28fa93cc9961..25f519f4475ff 100644 --- a/vscode-extension/package.json +++ b/vscode-extension/package.json @@ -39,6 +39,10 @@ { "command": "relay.stopCompiler", "title": "Relay: Stop Compiler" + }, + { + "command": "relay.copyOperation", + "title": "Relay: Copy Operation" } ], "configuration": { diff --git a/vscode-extension/src/commands/copyOperation.ts b/vscode-extension/src/commands/copyOperation.ts new file mode 100644 index 0000000000000..11fc2ca3399b8 --- /dev/null +++ b/vscode-extension/src/commands/copyOperation.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import * as semver from 'semver'; +import {window, env} from 'vscode'; +import {RequestType, TextDocumentPositionParams} from 'vscode-languageclient'; +import {RelayExtensionContext} from '../context'; + +export function handleCopyOperation(context: RelayExtensionContext): void { + const {binaryVersion} = context.relayBinaryExecutionOptions; + + if (binaryVersion) { + const isSupportedCompilerVersion = + semver.satisfies(binaryVersion, '>18.0') || + semver.prerelease(binaryVersion) != null; + + if (!isSupportedCompilerVersion) { + window.showWarningMessage( + 'Unsupported relay-compiler version. Requires >18.0.0', + ); + return; + } + } + + if (!context.client || !context.client.isRunning()) { + return; + } + + const activeEditor = window.activeTextEditor; + + if (!activeEditor) { + return; + } + + const request = new RequestType< + TextDocumentPositionParams, + PrintOperationResponse, + void + >('relay/printOperation'); + + const params: TextDocumentPositionParams = { + textDocument: {uri: activeEditor.document.uri.toString()}, + position: activeEditor.selection.active, + }; + + context.client.sendRequest(request, params).then(response => { + env.clipboard.writeText(response.operationText).then(() => { + window.showInformationMessage( + `Copied operation "${response.operationName}" to clipboard`, + ); + }); + }); +} + +type PrintOperationResponse = { + operationName: string; + operationText: string; +}; diff --git a/vscode-extension/src/commands/register.ts b/vscode-extension/src/commands/register.ts index e344826c8317a..aaa4a8e721c43 100644 --- a/vscode-extension/src/commands/register.ts +++ b/vscode-extension/src/commands/register.ts @@ -11,6 +11,7 @@ import {handleRestartLanguageServerCommand} from './restart'; import {handleShowOutputCommand} from './showOutput'; import {handleStartCompilerCommand} from './startCompiler'; import {handleStopCompilerCommand} from './stopCompiler'; +import {handleCopyOperation} from './copyOperation'; export function registerCommands(context: RelayExtensionContext) { context.extensionContext.subscriptions.push( @@ -30,5 +31,9 @@ export function registerCommands(context: RelayExtensionContext) { 'relay.showOutput', handleShowOutputCommand.bind(null, context), ), + commands.registerCommand( + 'relay.copyOperation', + handleCopyOperation.bind(null, context), + ), ); }