Skip to content
Closed
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
7 changes: 7 additions & 0 deletions lsp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,12 @@ settings:
will provide both type errors and other language features like go-to
definition, intellisense, hover, etc. Enable this option to keep type errors
from Pyrefly unchanged but use VSCode's Python extension for everything else.
- `python.pyrefly.analysis.disabledLanguageServices` [boolean: false]: an
analysis-level option that disables language-service features produced by
Pyrefly's analysis (for example: go-to definition, hover, and other
navigation/intelligence features) while leaving Pyrefly diagnostics
(type errors) intact. Use this when you want Pyrefly's type checking but
prefer the editor's language features or another provider for those
services.
- `pyrefly.lspPath` [string: '']: if your platform is not supported, you can
build pyrefly from source and specify the binary here.
63 changes: 63 additions & 0 deletions lsp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,69 @@
"off",
"verbose"
]
},
"python.pyrefly.analysis.disabledLanguageServices": {
"type": "object",
"default": {},
"description": "Disable specific language services. Set individual services to true to disable them.",
"properties": {
"hover": {
"type": "boolean",
"default": false
},
"documentSymbol": {
"type": "boolean",
"default": false
},
"workspaceSymbol": {
"type": "boolean",
"default": false
},
"inlayHint": {
"type": "boolean",
"default": false
},
"completion": {
"type": "boolean",
"default": false
},
"codeAction": {
"type": "boolean",
"default": false
},
"definition": {
"type": "boolean",
"default": false
},
"typeDefinition": {
"type": "boolean",
"default": false
},
"references": {
"type": "boolean",
"default": false
},
"documentHighlight": {
"type": "boolean",
"default": false
},
"rename": {
"type": "boolean",
"default": false
},
"codeLens": {
"type": "boolean",
"default": false
},
"semanticTokens": {
"type": "boolean",
"default": false
},
"signatureHelp": {
"type": "boolean",
"default": false
}
}
}
}
}
Expand Down
71 changes: 44 additions & 27 deletions pyrefly/lib/lsp/non_wasm/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1097,7 +1097,8 @@ impl Server {
params: crate::lsp::wasm::provide_type::ProvideTypeParams,
) -> Option<ProvideTypeResponse> {
let uri = &params.text_document.uri;
let handle = self.make_handle_if_enabled(uri)?;
// provide_type is not a standard LSP service so we will always keep it enabled
let handle = self.make_handle_if_enabled(uri, "textDocument/provideType")?;
provide_type(transaction, &handle, params.positions)
}

Expand Down Expand Up @@ -1608,31 +1609,46 @@ impl Server {

/// Create a handle with analysis config that decides language service behavior.
/// Return None if the workspace has language services disabled (and thus you shouldn't do anything).
///
/// `method` should be the LSP request METHOD string from lsp_types::request::* types
/// (e.g., GotoDefinition::METHOD, HoverRequest::METHOD, etc.)
fn make_handle_with_lsp_analysis_config_if_enabled(
&self,
uri: &Url,
method: &str,
) -> Option<(Handle, Option<LspAnalysisConfig>)> {
let path = uri.to_file_path().unwrap();
self.workspaces.get_with(path.clone(), |(_, workspace)| {
// Check if all language services are disabled
if workspace.disable_language_services {
eprintln!("Skipping request - language services disabled");
None
} else {
let module_path = if self.open_files.read().contains_key(&path) {
ModulePath::memory(path)
} else {
ModulePath::filesystem(path)
};
Some((
handle_from_module_path(&self.state, module_path),
workspace.lsp_analysis_config,
))
return None;
}

// Check if the specific service is disabled
if let Some(lsp_config) = workspace.lsp_analysis_config {
if let Some(disabled_services) = lsp_config.disabled_language_services {
if disabled_services.is_disabled(method) {
eprintln!("Skipping request - {} service disabled", method);
return None;
}
}
}

let module_path = if self.open_files.read().contains_key(&path) {
ModulePath::memory(path)
} else {
ModulePath::filesystem(path)
};
Some((
handle_from_module_path(&self.state, module_path),
workspace.lsp_analysis_config,
))
})
}

fn make_handle_if_enabled(&self, uri: &Url) -> Option<Handle> {
self.make_handle_with_lsp_analysis_config_if_enabled(uri)
fn make_handle_if_enabled(&self, uri: &Url, method: &str) -> Option<Handle> {
self.make_handle_with_lsp_analysis_config_if_enabled(uri, method)
.map(|(handle, _)| handle)
}

Expand All @@ -1642,7 +1658,7 @@ impl Server {
params: GotoDefinitionParams,
) -> Option<GotoDefinitionResponse> {
let uri = &params.text_document_position_params.text_document.uri;
let handle = self.make_handle_if_enabled(uri)?;
let handle = self.make_handle_if_enabled(uri, GotoDefinition::METHOD)?;
let info = transaction.get_module_info(&handle)?;
let range = info
.lined_buffer()
Expand All @@ -1667,7 +1683,7 @@ impl Server {
params: GotoTypeDefinitionParams,
) -> Option<GotoTypeDefinitionResponse> {
let uri = &params.text_document_position_params.text_document.uri;
let handle = self.make_handle_if_enabled(uri)?;
let handle = self.make_handle_if_enabled(uri, GotoTypeDefinition::METHOD)?;
let info = transaction.get_module_info(&handle)?;
let range = info
.lined_buffer()
Expand All @@ -1694,7 +1710,7 @@ impl Server {
) -> anyhow::Result<CompletionResponse> {
let uri = &params.text_document_position.text_document.uri;
let (handle, import_format) =
match self.make_handle_with_lsp_analysis_config_if_enabled(uri) {
match self.make_handle_with_lsp_analysis_config_if_enabled(uri, Completion::METHOD) {
None => {
return Ok(CompletionResponse::List(CompletionList {
is_incomplete: false,
Expand Down Expand Up @@ -1726,7 +1742,8 @@ impl Server {
params: CodeActionParams,
) -> Option<CodeActionResponse> {
let uri = &params.text_document.uri;
let (handle, lsp_config) = self.make_handle_with_lsp_analysis_config_if_enabled(uri)?;
let (handle, lsp_config) =
self.make_handle_with_lsp_analysis_config_if_enabled(uri, CodeActionRequest::METHOD)?;
let import_format = lsp_config.and_then(|c| c.import_format).unwrap_or_default();
let module_info = transaction.get_module_info(&handle)?;
let range = module_info.lined_buffer().from_lsp_range(params.range);
Expand Down Expand Up @@ -1758,7 +1775,7 @@ impl Server {
params: DocumentHighlightParams,
) -> Option<Vec<DocumentHighlight>> {
let uri = &params.text_document_position_params.text_document.uri;
let handle = self.make_handle_if_enabled(uri)?;
let handle = self.make_handle_if_enabled(uri, DocumentHighlightRequest::METHOD)?;
let info = transaction.get_module_info(&handle)?;
let position = info
.lined_buffer()
Expand All @@ -1784,7 +1801,7 @@ impl Server {
position: Position,
map_result: impl FnOnce(Vec<(Url, Vec<Range>)>) -> V + Send + Sync + 'static,
) {
let Some(handle) = self.make_handle_if_enabled(uri) else {
let Some(handle) = self.make_handle_if_enabled(uri, References::METHOD) else {
return self.send_response(new_response::<Option<V>>(request_id, Ok(None)));
};
let transaction = ide_transaction_manager.non_committable_transaction(&self.state);
Expand Down Expand Up @@ -1922,7 +1939,7 @@ impl Server {
params: TextDocumentPositionParams,
) -> Option<PrepareRenameResponse> {
let uri = &params.text_document.uri;
let handle = self.make_handle_if_enabled(uri)?;
let handle = self.make_handle_if_enabled(uri, Rename::METHOD)?;
let info = transaction.get_module_info(&handle)?;
let position = info.lined_buffer().from_lsp_position(params.position);
transaction
Expand All @@ -1936,7 +1953,7 @@ impl Server {
params: SignatureHelpParams,
) -> Option<SignatureHelp> {
let uri = &params.text_document_position_params.text_document.uri;
let handle = self.make_handle_if_enabled(uri)?;
let handle = self.make_handle_if_enabled(uri, SignatureHelpRequest::METHOD)?;
let info = transaction.get_module_info(&handle)?;
let position = info
.lined_buffer()
Expand All @@ -1946,7 +1963,7 @@ impl Server {

fn hover(&self, transaction: &Transaction<'_>, params: HoverParams) -> Option<Hover> {
let uri = &params.text_document_position_params.text_document.uri;
let handle = self.make_handle_if_enabled(uri)?;
let handle = self.make_handle_if_enabled(uri, HoverRequest::METHOD)?;
let info = transaction.get_module_info(&handle)?;
let position = info
.lined_buffer()
Expand All @@ -1963,7 +1980,7 @@ impl Server {
let uri = &params.text_document.uri;
let range = &params.range;
let (handle, lsp_analysis_config) =
self.make_handle_with_lsp_analysis_config_if_enabled(uri)?;
self.make_handle_with_lsp_analysis_config_if_enabled(uri, InlayHintRequest::METHOD)?;
let info = transaction.get_module_info(&handle)?;
let t = transaction.inlay_hints(
&handle,
Expand Down Expand Up @@ -2004,7 +2021,7 @@ impl Server {
params: SemanticTokensParams,
) -> Option<SemanticTokensResult> {
let uri = &params.text_document.uri;
let handle = self.make_handle_if_enabled(uri)?;
let handle = self.make_handle_if_enabled(uri, SemanticTokensFullRequest::METHOD)?;
Some(SemanticTokensResult::Tokens(SemanticTokens {
result_id: None,
data: transaction
Expand All @@ -2019,7 +2036,7 @@ impl Server {
params: SemanticTokensRangeParams,
) -> Option<SemanticTokensRangeResult> {
let uri = &params.text_document.uri;
let handle = self.make_handle_if_enabled(uri)?;
let handle = self.make_handle_if_enabled(uri, SemanticTokensRangeRequest::METHOD)?;
let module_info = transaction.get_module_info(&handle)?;
let range = module_info.lined_buffer().from_lsp_range(params.range);
Some(SemanticTokensRangeResult::Tokens(SemanticTokens {
Expand Down Expand Up @@ -2052,7 +2069,7 @@ impl Server {
{
return None;
}
let handle = self.make_handle_if_enabled(uri)?;
let handle = self.make_handle_if_enabled(uri, DocumentSymbolRequest::METHOD)?;
transaction.symbols(&handle)
}

Expand Down
72 changes: 70 additions & 2 deletions pyrefly/lib/lsp/non_wasm/workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ struct PyreflyClientConfig {
display_type_errors: Option<DisplayTypeErrors>,
disable_language_services: Option<bool>,
extra_paths: Option<Vec<PathBuf>>,
#[serde(default, deserialize_with = "deserialize_analysis")]
analysis: Option<LspAnalysisConfig>,
}

#[derive(Clone, Copy, Debug, Deserialize)]
Expand All @@ -169,6 +171,61 @@ pub enum DiagnosticMode {
OpenFilesOnly,
}

/// Configuration for which language services should be disabled
#[derive(Clone, Copy, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DisabledLanguageServices {
#[serde(default)]
pub definition: bool,
#[serde(default)]
pub type_definition: bool,
#[serde(default)]
pub code_action: bool,
#[serde(default)]
pub completion: bool,
#[serde(default)]
pub document_highlight: bool,
#[serde(default)]
pub references: bool,
#[serde(default)]
pub rename: bool,
#[serde(default)]
pub signature_help: bool,
#[serde(default)]
pub hover: bool,
#[serde(default)]
pub inlay_hint: bool,
#[serde(default)]
pub document_symbol: bool,
#[serde(default)]
pub semantic_tokens: bool,
}

impl DisabledLanguageServices {
/// Check if a language service is disabled based on the LSP request METHOD string
/// Uses the METHOD constants from lsp_types::request::* types
pub fn is_disabled(&self, method: &str) -> bool {
match method {
"textDocument/provideType" => false, // Always enabled
"textDocument/definition" => self.definition,
"textDocument/typeDefinition" => self.type_definition,
"textDocument/codeAction" => self.code_action,
"textDocument/completion" => self.completion,
"textDocument/documentHighlight" => self.document_highlight,
"textDocument/references" => self.references,
"textDocument/rename" => self.rename,
"textDocument/signatureHelp" => self.signature_help,
"textDocument/hover" => self.hover,
"textDocument/inlayHint" => self.inlay_hint,
"textDocument/documentSymbol" => self.document_symbol,
"textDocument/semanticTokens/full" | "textDocument/semanticTokens/range" => {
self.semantic_tokens
}
_ => false, // Unknown methods are not disabled
}
}
}

/// https://code.visualstudio.com/docs/python/settings-reference#_pylance-language-server
#[derive(Clone, Copy, Debug, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
Expand All @@ -177,6 +234,8 @@ pub struct LspAnalysisConfig {
pub diagnostic_mode: Option<DiagnosticMode>,
pub import_format: Option<ImportFormat>,
pub inlay_hints: Option<InlayHintConfig>,
#[serde(default)]
pub disabled_language_services: Option<DisabledLanguageServices>,
}

fn deserialize_analysis<'de, D>(deserializer: D) -> Result<Option<LspAnalysisConfig>, D::Error>
Expand Down Expand Up @@ -282,6 +341,7 @@ impl Workspaces {
self.update_pythonpath(modified, scope_uri, &python_path);
}

let mut analysis_handled = false;
if let Some(pyrefly) = config.pyrefly {
if let Some(extra_paths) = pyrefly.extra_paths {
self.update_search_paths(modified, scope_uri, extra_paths);
Expand All @@ -290,9 +350,17 @@ impl Workspaces {
self.update_disable_language_services(scope_uri, disable_language_services);
}
self.update_display_type_errors(modified, scope_uri, pyrefly.display_type_errors);
// Handle analysis config nested under pyrefly (e.g., pyrefly.analysis.disabledLanguageServices)
if let Some(analysis) = pyrefly.analysis {
self.update_ide_settings(modified, scope_uri, analysis);
analysis_handled = true;
}
}
if let Some(analysis) = config.analysis {
self.update_ide_settings(modified, scope_uri, analysis);
// Also handle analysis at top level for backward compatibility (only if not already handled)
if !analysis_handled {
if let Some(analysis) = config.analysis {
self.update_ide_settings(modified, scope_uri, analysis);
}
}
}

Expand Down
Loading