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
This commit is contained in:
Lukas Wirth authored and GitHub committed 2026-07-22 13:25:49 +00:00
1 parent 45669409fb
commit df5d437085
7 files changed
+173 -8

No files matched your search

+5
View File
@@ -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.
+2
View File
@@ -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,
+36 -6
View File
@@ -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::<Vec<_>>();
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::<Vec<_>>()
};
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<Buffer>,
@@ -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
+11
View File
@@ -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
@@ -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::<lsp::notification::DidOpenTextDocument>()
.await
.text_document
.uri
.as_str(),
uri!("file:///dir/normal.js")
);
cx.executor().run_until_parked();
assert!(
fake_js_server
.receive_notification::<lsp::notification::DidOpenTextDocument>()
.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::<lsp::notification::DidOpenTextDocument>()
.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);
+5
View File
@@ -234,6 +234,11 @@ pub struct GlobalLspSettingsContent {
///
/// Default: `120`
pub request_timeout: Option<u64>,
/// 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<u32>,
/// Settings for language server notifications
pub notifications: Option<LspNotificationSettingsContent>,
/// Rules for rendering LSP semantic tokens.
+2
View File
@@ -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.