From df5d437085c3d1428759faff1ad6eaa8a3fa896a Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Wed, 22 Jul 2026 13:25:49 +0000 Subject: [PATCH] Disable LSP for files with very long lines (#61447) Release Notes: - Fixed ui stutters when opening large single line files like minified javascript due to running LSP requests against them --- assets/settings/default.json | 5 + crates/editor/src/editor_tests.rs | 2 + crates/project/src/lsp_store.rs | 42 ++++++- crates/project/src/project_settings.rs | 11 ++ .../tests/integration/project_tests.rs | 114 +++++++++++++++++- crates/settings_content/src/project.rs | 5 + docs/src/reference/all-settings.md | 2 + 7 files changed, 173 insertions(+), 8 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 397960df19..af1f663979 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -2561,6 +2561,11 @@ // // Default: 120 "request_timeout": 120, + // The maximum line length a buffer may contain before language server features are disabled + // for the entire buffer. + // + // Default: 20000 + "max_buffer_line_length": 20000, "notifications": { // Timeout in milliseconds for automatically dismissing language server notifications. // Set to 0 to disable auto-dismiss. diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index d8af07824e..d7c68d6399 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -35681,6 +35681,7 @@ async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) { store.update_user_settings(cx, &|settings: &mut SettingsContent| { settings.global_lsp_settings = Some(GlobalLspSettingsContent { request_timeout: Some(BASE_TIMEOUT_SECS), + max_buffer_line_length: None, button: Some(true), notifications: None, semantic_token_rules: None, @@ -35791,6 +35792,7 @@ async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) { store.update_user_settings(cx, |settings| { settings.global_lsp_settings = Some(GlobalLspSettingsContent { request_timeout: Some(BASE_TIMEOUT_SECS * 4), + max_buffer_line_length: None, button: Some(true), notifications: None, semantic_token_rules: None, diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 593c1dd66b..5b911dc3c7 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1621,12 +1621,19 @@ impl LocalLspStore { let (adapters_and_servers, settings, request_timeout) = lsp_store.update(cx, |lsp_store, cx| { buffer.handle.update(cx, |buffer, cx| { - let adapters_and_servers = lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>(); + let adapters_and_servers = + if LocalLspStore::language_server_line_length_limit_exceeded(buffer, cx) + .is_some() + { + Vec::new() + } else { + lsp_store + .as_local() + .unwrap() + .language_servers_for_buffer(buffer, cx) + .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) + .collect::>() + }; let settings = LanguageSettings::for_buffer(buffer, cx).into_owned(); let request_timeout = ProjectSettings::get_global(cx) .global_lsp_settings @@ -2877,6 +2884,15 @@ impl LocalLspStore { }); } + fn language_server_line_length_limit_exceeded(buffer: &Buffer, cx: &App) -> Option<(u32, u32)> { + let maximum_line_length = ProjectSettings::get_global(cx) + .global_lsp_settings + .max_buffer_line_length; + let longest_line_length = buffer.text_snapshot().text_summary().longest_row_chars; + (longest_line_length > maximum_line_length) + .then_some((longest_line_length, maximum_line_length)) + } + fn register_buffer_with_language_servers( &mut self, buffer_handle: &Entity, @@ -2897,6 +2913,17 @@ impl LocalLspStore { } let abs_path = file.abs_path(cx); + if let Some((longest_line_length, maximum_line_length)) = + Self::language_server_line_length_limit_exceeded(buffer, cx) + { + log::debug!( + "not registering {} with language servers because its longest line has {} characters, exceeding the configured limit of {}", + abs_path.display(), + longest_line_length, + maximum_line_length, + ); + return; + } let Some(uri) = file_path_to_lsp_url(&abs_path).log_err() else { return; }; @@ -12230,6 +12257,9 @@ impl LspStore { Some(language) => language, None => continue, }; + if LocalLspStore::language_server_line_length_limit_exceeded(buffer, cx).is_some() { + continue; + } if !worktrees_using_server.contains(&file.worktree.read(cx).id()) || !lsp_adapters diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 68e40162de..86d0d43a4a 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -133,6 +133,10 @@ pub struct GlobalLspSettings { /// /// Default: `120` pub request_timeout: u64, + /// The maximum line length a buffer may contain before language server features are disabled for the entire buffer. + /// + /// Default: `20000` + pub max_buffer_line_length: u32, pub notifications: LspNotificationSettings, /// Rules for highlighting semantic tokens. @@ -144,6 +148,7 @@ impl Default for GlobalLspSettings { Self { button: true, request_timeout: DEFAULT_LSP_REQUEST_TIMEOUT_SECS, + max_buffer_line_length: 20_000, notifications: LspNotificationSettings::default(), semantic_token_rules: SemanticTokenRules::default(), } @@ -737,6 +742,12 @@ impl Settings for ProjectSettings { .unwrap() .request_timeout .unwrap(), + max_buffer_line_length: content + .global_lsp_settings + .as_ref() + .unwrap() + .max_buffer_line_length + .unwrap(), notifications: LspNotificationSettings { dismiss_timeout_ms: content .global_lsp_settings diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index 6857ce680a..e4796b29a1 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -26,7 +26,7 @@ use buffer_diff::{ use collections::{BTreeSet, HashMap, HashSet}; use encoding_rs; use fs::{FakeFs, PathEventKind, RealFs}; -use futures::{StreamExt, future}; +use futures::{FutureExt as _, StreamExt, future}; use git::{ GitHostingProviderRegistry, repository::{RepoPath, repo_path}, @@ -66,7 +66,7 @@ use project::{ }; use rand::{Rng as _, rngs::StdRng}; use serde_json::json; -use settings::SettingsStore; +use settings::{GlobalLspSettingsContent, SettingsStore}; #[cfg(target_os = "linux")] use settings::{LocalSettingsKind, LocalSettingsPath}; #[cfg(not(windows))] @@ -3555,6 +3555,116 @@ async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_long_lines_disable_language_servers(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({ + "minified.js": "x".repeat(20_001), + "normal.js": "const answer = 42;", + }), + ) + .await; + + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let mut fake_js_servers = language_registry.register_fake_lsp( + "JavaScript", + FakeLspAdapter { + name: "js-lsp", + ..Default::default() + }, + ); + language_registry.add(js_lang()); + + let _minified_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/dir/minified.js"), cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + assert!(fake_js_servers.next().now_or_never().is_none()); + + let _normal_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/dir/normal.js"), cx) + }) + .await + .unwrap(); + let mut fake_js_server = fake_js_servers.next().await.unwrap(); + assert_eq!( + fake_js_server + .receive_notification::() + .await + .text_document + .uri + .as_str(), + uri!("file:///dir/normal.js") + ); + cx.executor().run_until_parked(); + assert!( + fake_js_server + .receive_notification::() + .now_or_never() + .is_none() + ); +} + +#[gpui::test] +async fn test_max_buffer_line_length_can_be_overridden(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree(path!("/dir"), json!({ "minified.js": "x".repeat(20_001) })) + .await; + + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; + let language_registry = project.read_with(cx, |project, _| project.languages().clone()); + let mut fake_js_servers = language_registry.register_fake_lsp( + "JavaScript", + FakeLspAdapter { + name: "js-lsp", + ..Default::default() + }, + ); + language_registry.add(js_lang()); + + let _minified_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer_with_lsp(path!("/dir/minified.js"), cx) + }) + .await + .unwrap(); + cx.executor().run_until_parked(); + assert!(fake_js_servers.next().now_or_never().is_none()); + + cx.update(|cx| { + SettingsStore::update_global(cx, |settings, cx| { + settings.update_user_settings(cx, |settings| { + settings.global_lsp_settings = Some(GlobalLspSettingsContent { + max_buffer_line_length: Some(20_001), + ..Default::default() + }); + }); + }) + }); + + let mut fake_js_server = fake_js_servers.next().await.unwrap(); + assert_eq!( + fake_js_server + .receive_notification::() + .await + .text_document + .uri + .as_str(), + uri!("file:///dir/minified.js") + ); +} + #[gpui::test] async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) { init_test(cx); diff --git a/crates/settings_content/src/project.rs b/crates/settings_content/src/project.rs index f178e59efc..84b41706e5 100644 --- a/crates/settings_content/src/project.rs +++ b/crates/settings_content/src/project.rs @@ -234,6 +234,11 @@ pub struct GlobalLspSettingsContent { /// /// Default: `120` pub request_timeout: Option, + /// The maximum line length a buffer may contain before language server features are disabled for the entire buffer. + /// + /// Default: `20000` + #[schemars(range(min = 1))] + pub max_buffer_line_length: Option, /// Settings for language server notifications pub notifications: Option, /// Rules for rendering LSP semantic tokens. diff --git a/docs/src/reference/all-settings.md b/docs/src/reference/all-settings.md index 13123caf5b..25024fc908 100644 --- a/docs/src/reference/all-settings.md +++ b/docs/src/reference/all-settings.md @@ -1851,6 +1851,7 @@ While other options may be changed at a runtime and should be placed under `sett "global_lsp_settings": { "button": true, "request_timeout": 120, + "max_buffer_line_length": 20000, "notifications": { // Timeout in milliseconds for automatically dismissing language server notifications. // Set to 0 to disable auto-dismiss. @@ -1864,6 +1865,7 @@ While other options may be changed at a runtime and should be placed under `sett - `button`: Whether to show the LSP status button in the status bar - `request_timeout`: The maximum amount of time to wait for responses from language servers, in seconds. A value of `0` will result in no timeout being applied (causing all LSP responses to wait indefinitely until completed). Default: `120` +- `max_buffer_line_length`: The maximum line length a buffer may contain before Zed disables all language server features for that entire buffer. If any line exceeds this value, Zed does not open the buffer with language servers or send them buffer-specific requests. Default: `20000` - `notifications`: Notification-related settings. - `dismiss_timeout_ms`: Timeout in milliseconds for automatically dismissing language server notifications. Set to 0 to disable auto-dismiss.