Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
5 changes: 5 additions & 0 deletions crates/ty_project/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ impl ProjectDatabase {
self.project().check_file(self, file)
}

/// Returns the check mode for the project.
pub fn check_mode(&self) -> CheckMode {
self.project().check_mode(self)
}

/// Set the check mode for the project.
pub fn set_check_mode(&mut self, mode: CheckMode) {
tracing::debug!("Updating project to check {mode}");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use lsp_types::{ClientCapabilities, MarkupKind};
use lsp_types::{ClientCapabilities, DiagnosticOptions, MarkupKind};

bitflags::bitflags! {
/// Represents the resolved client capabilities for the language server.
Expand All @@ -17,6 +17,8 @@ bitflags::bitflags! {
const SIGNATURE_LABEL_OFFSET_SUPPORT = 1 << 8;
const SIGNATURE_ACTIVE_PARAMETER_SUPPORT = 1 << 9;
const HIERARCHICAL_DOCUMENT_SYMBOL_SUPPORT = 1 << 10;
const FILE_WATCHER_SUPPORT = 1 << 11;
const DIAGNOSTIC_DYNAMIC_REGISTRATION = 1 << 12;
}
}

Expand Down Expand Up @@ -76,6 +78,16 @@ impl ResolvedClientCapabilities {
self.contains(Self::HIERARCHICAL_DOCUMENT_SYMBOL_SUPPORT)
}

/// Returns `true` if the client supports file watcher capabilities.
pub(crate) const fn supports_file_watcher(self) -> bool {
self.contains(Self::FILE_WATCHER_SUPPORT)
}

/// Returns `true` if the client supports dynamic registration for diagnostic capabilities.
pub(crate) const fn supports_diagnostic_dynamic_registration(self) -> bool {
self.contains(Self::DIAGNOSTIC_DYNAMIC_REGISTRATION)
}

pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self {
let mut flags = Self::empty();

Expand All @@ -96,10 +108,24 @@ impl ResolvedClientCapabilities {
flags |= Self::INLAY_HINT_REFRESH;
}

if workspace
.and_then(|workspace| workspace.did_change_watched_files?.dynamic_registration)
.unwrap_or_default()
{
flags |= Self::FILE_WATCHER_SUPPORT;
}

if text_document.is_some_and(|text_document| text_document.diagnostic.is_some()) {
flags |= Self::PULL_DIAGNOSTICS;
}

if text_document
.and_then(|text_document| text_document.diagnostic.as_ref()?.dynamic_registration)
.unwrap_or_default()
{
flags |= Self::DIAGNOSTIC_DYNAMIC_REGISTRATION;
}

if text_document
.and_then(|text_document| text_document.type_definition?.link_support)
.unwrap_or_default()
Expand Down Expand Up @@ -194,3 +220,13 @@ impl ResolvedClientCapabilities {
flags
}
}

/// Creates the default [`DiagnosticOptions`] for the server.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a bit out of place. The function doesn't contain any code related to capabilities

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does, the DiagnosticOptions are the server capability for document diagnostic.

pub(crate) fn server_diagnostic_options(workspace_diagnostics: bool) -> DiagnosticOptions {
DiagnosticOptions {
identifier: Some(crate::DIAGNOSTIC_NAME.to_string()),
inter_file_dependencies: true,
workspace_diagnostics,
..Default::default()
}
}
3 changes: 2 additions & 1 deletion crates/ty_server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ use ruff_db::system::{OsSystem, SystemPathBuf};

pub use crate::logging::{LogLevel, init_logging};
pub use crate::server::Server;
pub use crate::session::ClientOptions;
pub use crate::session::{ClientOptions, DiagnosticMode};
pub use document::{NotebookDocument, PositionEncoding, TextDocument};
pub(crate) use session::{DocumentQuery, Session};

mod capabilities;
mod document;
mod logging;
mod server;
Expand Down
119 changes: 59 additions & 60 deletions crates/ty_server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

use self::schedule::spawn_main_loop;
use crate::PositionEncoding;
use crate::session::{AllOptions, ClientOptions, DiagnosticMode, Session};
use crate::capabilities::{ResolvedClientCapabilities, server_diagnostic_options};
use crate::session::{InitializationOptions, Session};
use lsp_server::Connection;
use lsp_types::{
ClientCapabilities, DeclarationCapability, DiagnosticOptions, DiagnosticServerCapabilities,
ClientCapabilities, DeclarationCapability, DiagnosticServerCapabilities,
HoverProviderCapability, InitializeParams, InlayHintOptions, InlayHintServerCapabilities,
MessageType, SelectionRangeProviderCapability, SemanticTokensLegend, SemanticTokensOptions,
SemanticTokensServerCapabilities, ServerCapabilities, SignatureHelpOptions,
Expand All @@ -29,7 +30,7 @@ pub(crate) type Result<T> = std::result::Result<T, api::Error>;

pub struct Server {
connection: Connection,
client_capabilities: ClientCapabilities,
resolved_client_capabilities: ResolvedClientCapabilities,
worker_threads: NonZeroUsize,
main_loop_receiver: MainLoopReceiver,
main_loop_sender: MainLoopSender,
Expand All @@ -44,23 +45,38 @@ impl Server {
initialize_logging: bool,
) -> crate::Result<Self> {
let (id, init_value) = connection.initialize_start()?;
let init_params: InitializeParams = serde_json::from_value(init_value)?;

let AllOptions {
global: global_options,
workspace: mut workspace_options,
} = AllOptions::from_value(
init_params
.initialization_options
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())),
);

let client_capabilities = init_params.capabilities;
let InitializeParams {
initialization_options,
capabilities: client_capabilities,
workspace_folders,
..
} = serde_json::from_value(init_value)?;

let (initialization_options, deserialization_error) =
InitializationOptions::from_value(initialization_options);

if initialize_logging {
crate::logging::init_logging(
initialization_options.log_level.unwrap_or_default(),
initialization_options.log_file.as_deref(),
);
}

if let Some(error) = deserialization_error {
tracing::error!(
"Failed to deserialize initialization options: {error}. \
Using default initialization options."
);
}

let resolved_client_capabilities = ResolvedClientCapabilities::new(&client_capabilities);
let position_encoding = Self::find_best_position_encoding(&client_capabilities);
let server_capabilities =
Self::server_capabilities(position_encoding, global_options.diagnostic_mode());
Self::server_capabilities(position_encoding, resolved_client_capabilities);

let version = ruff_db::program_version().unwrap_or("Unknown");
tracing::debug!("Version: {version}");

connection.initialize_finish(
id,
Expand All @@ -78,37 +94,14 @@ impl Server {
let (main_loop_sender, main_loop_receiver) = crossbeam::channel::bounded(32);
let client = Client::new(main_loop_sender.clone(), connection.sender.clone());

if initialize_logging {
crate::logging::init_logging(
global_options.tracing.log_level.unwrap_or_default(),
global_options.tracing.log_file.as_deref(),
);
}

tracing::debug!("Version: {version}");

let mut workspace_for_url = |url: Url| {
let Some(workspace_settings) = workspace_options.as_mut() else {
return (url, ClientOptions::default());
};
let settings = workspace_settings.remove(&url).unwrap_or_else(|| {
tracing::warn!(
"No workspace options found for {}, using default options",
url
);
ClientOptions::default()
});
(url, settings)
};

let workspaces = init_params
.workspace_folders
// Get workspace URLs without settings - settings will come from workspace/configuration
let workspace_urls = workspace_folders
.filter(|folders| !folders.is_empty())
.map(|folders| {
folders
.into_iter()
.map(|folder| workspace_for_url(folder.uri))
.collect()
.map(|folder| folder.uri)
.collect::<Vec<_>>()
})
.or_else(|| {
let current_dir = native_system
Expand All @@ -122,7 +115,7 @@ impl Server {
current_dir.display()
);
let uri = Url::from_file_path(current_dir).ok()?;
Some(vec![workspace_for_url(uri)])
Some(vec![uri])
})
.ok_or_else(|| {
anyhow::anyhow!(
Expand All @@ -131,19 +124,19 @@ impl Server {
)
})?;

let workspaces = if workspaces.len() > 1 {
let first_workspace = workspaces.into_iter().next().unwrap();
let workspace_urls = if workspace_urls.len() > 1 {
let first_workspace = workspace_urls.into_iter().next().unwrap();
tracing::warn!(
"Multiple workspaces are not yet supported, using the first workspace: {}",
&first_workspace.0
&first_workspace
);
client.show_warning_message(format_args!(
"Multiple workspaces are not yet supported, using the first workspace: {}",
&first_workspace.0,
&first_workspace,
));
vec![first_workspace]
} else {
workspaces
workspace_urls
};

Ok(Self {
Expand All @@ -152,13 +145,12 @@ impl Server {
main_loop_receiver,
main_loop_sender,
session: Session::new(
&client_capabilities,
resolved_client_capabilities,
position_encoding,
global_options,
workspaces,
workspace_urls,
native_system,
)?,
client_capabilities,
resolved_client_capabilities,
})
}

Expand Down Expand Up @@ -187,19 +179,26 @@ impl Server {
.unwrap_or_default()
}

// TODO: Move this to `capabilities.rs`?
fn server_capabilities(
position_encoding: PositionEncoding,
diagnostic_mode: DiagnosticMode,
resolved_client_capabilities: ResolvedClientCapabilities,
) -> ServerCapabilities {
let diagnostic_provider =
if resolved_client_capabilities.supports_diagnostic_dynamic_registration() {
// If the client supports dynamic registration, we will register the diagnostic
// capabilities dynamically based on the `ty.diagnosticMode` setting.
None
} else {
// Otherwise, we always advertise support for workspace diagnostics.
Some(DiagnosticServerCapabilities::Options(
server_diagnostic_options(true),
))
};

ServerCapabilities {
position_encoding: Some(position_encoding.into()),
diagnostic_provider: Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
identifier: Some(crate::DIAGNOSTIC_NAME.into()),
inter_file_dependencies: true,
// TODO: Dynamically register for workspace diagnostics.
workspace_diagnostics: diagnostic_mode.is_workspace(),
..Default::default()
})),
diagnostic_provider,
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
open_close: Some(true),
Expand Down
7 changes: 1 addition & 6 deletions crates/ty_server/src/server/api/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,7 @@ pub(crate) fn publish_settings_diagnostics(
// Note we DO NOT respect the fact that clients support pulls because these are
// files they *specifically* won't pull diagnostics from us for, because we don't
// claim to be an LSP for them.
let has_workspace_diagnostics = session
.workspaces()
.for_path(&path)
.map(|workspace| workspace.settings().diagnostic_mode().is_workspace())
.unwrap_or(false);
if has_workspace_diagnostics {
if session.global_settings().diagnostic_mode().is_workspace() {
return;
}

Expand Down
25 changes: 18 additions & 7 deletions crates/ty_server/src/server/api/notifications/did_close.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
// interned in the lookup table (`Files`).
tracing::warn!("Salsa file does not exists for {}", system_path);
}

// For non-virtual files, we clear diagnostics if:
//
// 1. The file does not belong to any workspace e.g., opening a random file from
// outside the workspace because closing it acts like the file doesn't exists
// 2. The diagnostic mode is set to open-files only
if session.workspaces().for_path(system_path).is_none()
|| session
.global_settings()
.diagnostic_mode()
.is_open_files_only()
{
clear_diagnostics(&key, client);
}
}
AnySystemPath::SystemVirtual(virtual_path) => {
if let Some(virtual_file) = db.files().try_virtual_file(virtual_path) {
Expand All @@ -61,14 +75,11 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
} else {
tracing::warn!("Salsa virtual file does not exists for {}", virtual_path);
}
}
}

if !session.global_settings().diagnostic_mode().is_workspace() {
// The server needs to clear the diagnostics regardless of whether the client supports
// pull diagnostics or not. This is because the client only has the capability to fetch
// the diagnostics but does not automatically clear them when a document is closed.
clear_diagnostics(&key, client);
// Always clear diagnostics for virtual files, as they don't really exist on disk
// which means closing them is like deleting the file.
clear_diagnostics(&key, client);
}
}

Ok(())
Expand Down
5 changes: 4 additions & 1 deletion crates/ty_server/src/server/api/requests/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
) -> crate::server::Result<Option<CompletionResponse>> {
let start = Instant::now();

if snapshot.client_settings().is_language_services_disabled() {
if snapshot
.workspace_settings()
.is_language_services_disabled()
{
return Ok(None);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ impl BackgroundDocumentRequestHandler for DocumentHighlightRequestHandler {
_client: &Client,
params: DocumentHighlightParams,
) -> crate::server::Result<Option<Vec<DocumentHighlight>>> {
if snapshot.client_settings().is_language_services_disabled() {
if snapshot
.workspace_settings()
.is_language_services_disabled()
{
return Ok(None);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ impl BackgroundDocumentRequestHandler for DocumentSymbolRequestHandler {
_client: &Client,
params: DocumentSymbolParams,
) -> crate::server::Result<Option<lsp_types::DocumentSymbolResponse>> {
if snapshot.client_settings().is_language_services_disabled() {
if snapshot
.workspace_settings()
.is_language_services_disabled()
{
return Ok(None);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ impl BackgroundDocumentRequestHandler for GotoDeclarationRequestHandler {
_client: &Client,
params: GotoDeclarationParams,
) -> crate::server::Result<Option<GotoDefinitionResponse>> {
if snapshot.client_settings().is_language_services_disabled() {
if snapshot
.workspace_settings()
.is_language_services_disabled()
{
return Ok(None);
}

Expand Down
Loading
Loading