diff --git a/assets/settings/default.json b/assets/settings/default.json index 05d9b592979f19184f6e1b9b9cd6c7b02c603ca1..7ef69bc1675947ba3d07dc7b523658902459996b 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1346,6 +1346,12 @@ "hard_tabs": false, // How many columns a tab should occupy. "tab_size": 4, + // Number of lines to search for modelines at the beginning and end of files. + // Modelines contain editor directives (e.g., vim/emacs settings) that configure + // the editor behavior for specific files. + // + // A value of 0 disables modelines support. + "modeline_lines": 5, // What debuggers are preferred by default for all languages. "debuggers": [], // Whether to enable word diff highlighting in the editor. diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index e11d86196ec6367ee6d2ded709c3ba9e100da514..3b143efbfdbd9c1ef636f76db795cd2f5b3b43e7 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -2588,11 +2588,8 @@ impl AcpThread { let format_on_save = buffer.update(cx, |buffer, cx| { buffer.edit(edits, None, cx); - let settings = language::language_settings::language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ); + let settings = + language::language_settings::LanguageSettings::for_buffer(buffer, cx); settings.format_on_save != FormatOnSave::Off }); diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs index 2a199744feb33fd9d7002e100e324d3d3524aebb..6f9ad092d428389f0d83383060010446dd2c2dff 100644 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs +++ b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs @@ -550,7 +550,7 @@ impl Default for EditorStyle { } pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { - let show_background = language_settings::language_settings(None, None, cx) + let show_background = language_settings::language_settings(cx).get() .inlay_hints .show_background; @@ -5989,7 +5989,7 @@ impl Editor { let file = buffer.file(); - if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions { + if !language_settings(cx).buffer(buffer).get().show_edit_predictions { return EditPredictionSettings::Disabled; }; @@ -18801,7 +18801,7 @@ fn choose_completion_range( } = &completion.source { let completion_mode_setting = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + language_settings(cx).buffer(buffer).get() .completions .lsp_insert_mode; @@ -19850,7 +19850,7 @@ fn inlay_hint_settings( ) -> InlayHintSettings { let file = snapshot.file_at(location); let language = snapshot.language_at(location).map(|l| l.name()); - language_settings(language, file, cx).inlay_hints + language_settings(cx).language(language).file(file).get().inlay_hints } fn consume_contiguous_rows( diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs index 0b4d7ce5eb94b79ed8f822e14b76c191788afcf9..b0da850399c1fc8c871c7559685abccd4251cd40 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -419,17 +419,6 @@ impl AgentTool for EditFileTool { EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, EditAgentOutputEvent::ResolvingEditRange(range) => { diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx)); - // if !emitted_location { - // let line = buffer.update(cx, |buffer, _cx| { - // range.start.to_point(&buffer.snapshot()).row - // }).ok(); - // if let Some(abs_path) = abs_path.clone() { - // event_stream.update_fields(ToolCallUpdateFields { - // locations: Some(vec![ToolCallLocation { path: abs_path, line }]), - // ..Default::default() - // }); - // } - // } } } } @@ -437,11 +426,7 @@ impl AgentTool for EditFileTool { output.await?; let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| { - let settings = language_settings::language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ); + let settings = language_settings::LanguageSettings::for_buffer(buffer, cx); settings.format_on_save != FormatOnSave::Off }); diff --git a/crates/agent/src/tools/streaming_edit_file_tool.rs b/crates/agent/src/tools/streaming_edit_file_tool.rs index c0e4e39753e436511fcdd675cbe8027d41bf1e73..e62e47d404364f8aaddef3b4329cf93e1295370b 100644 --- a/crates/agent/src/tools/streaming_edit_file_tool.rs +++ b/crates/agent/src/tools/streaming_edit_file_tool.rs @@ -626,11 +626,7 @@ impl EditSession { } let format_on_save_enabled = self.buffer.read_with(cx, |buffer, cx| { - let settings = language_settings::language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ); + let settings = language_settings::LanguageSettings::for_buffer(buffer, cx); settings.format_on_save != FormatOnSave::Off }); diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index c0f62ed8fc1c990b3bb4aaef5fff5ae23bebff86..2ea1c349074dcadf9723e11df154f3d9e9bf3d75 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -2,8 +2,8 @@ use futures::channel::oneshot; use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task}; use language::{ - Capability, Diff, DiffOptions, File, Language, LanguageName, LanguageRegistry, - language_settings::language_settings, word_diff_ranges, + Capability, Diff, DiffOptions, Language, LanguageName, LanguageRegistry, + language_settings::LanguageSettings, word_diff_ranges, }; use rope::Rope; use std::{ @@ -1067,7 +1067,6 @@ impl BufferDiffInner { } fn build_diff_options( - file: Option<&Arc>, language: Option, language_scope: Option, cx: &App, @@ -1083,7 +1082,7 @@ fn build_diff_options( } } - language_settings(language, file, cx) + LanguageSettings::resolve(None, language.as_ref(), cx) .word_diff_enabled .then_some(DiffOptions { language_scope, @@ -1656,7 +1655,6 @@ impl BufferDiff { let base_text_changed = base_text_change.is_some(); let compute_base_text_edits = base_text_change == Some(true); let diff_options = build_diff_options( - None, language.as_ref().map(|l| l.name()), language.as_ref().map(|l| l.default_scope()), cx, diff --git a/crates/collab/tests/integration/editor_tests.rs b/crates/collab/tests/integration/editor_tests.rs index 1590f498308c74125c7672595cb7510b6653e9b1..2ce3abf48f12b2ede1f0340e2e438d3df0704985 100644 --- a/crates/collab/tests/integration/editor_tests.rs +++ b/crates/collab/tests/integration/editor_tests.rs @@ -23,7 +23,7 @@ use gpui::{ VisualTestContext, }; use indoc::indoc; -use language::{FakeLspAdapter, language_settings::language_settings, rust_lang}; +use language::{FakeLspAdapter, language_settings::LanguageSettings, rust_lang}; use lsp::DEFAULT_LSP_REQUEST_TIMEOUT; use multi_buffer::{AnchorRangeExt as _, MultiBufferRow}; use pretty_assertions::assert_eq; @@ -4036,6 +4036,8 @@ async fn test_collaborating_with_external_editorconfig( .await .unwrap(); + project_a.update(cx_a, |project, _| project.languages().add(rust_lang())); + // Open buffer on client A let buffer_a = project_a .update(cx_a, |p, cx| { @@ -4048,13 +4050,13 @@ async fn test_collaborating_with_external_editorconfig( // Verify client A sees external editorconfig settings cx_a.read(|cx| { - let file = buffer_a.read(cx).file(); - let settings = language_settings(Some("Rust".into()), file, cx); + let settings = LanguageSettings::for_buffer(&buffer_a.read(cx), cx); assert_eq!(Some(settings.tab_size), NonZeroU32::new(5)); }); // Client B joins the project let project_b = client_b.join_remote_project(project_id, cx_b).await; + project_b.update(cx_b, |project, _| project.languages().add(rust_lang())); let buffer_b = project_b .update(cx_b, |p, cx| { p.open_buffer((worktree_id, rel_path("src/main.rs")), cx) @@ -4066,8 +4068,7 @@ async fn test_collaborating_with_external_editorconfig( // Verify client B also sees external editorconfig settings cx_b.read(|cx| { - let file = buffer_b.read(cx).file(); - let settings = language_settings(Some("Rust".into()), file, cx); + let settings = LanguageSettings::for_buffer(&buffer_b.read(cx), cx); assert_eq!(Some(settings.tab_size), NonZeroU32::new(5)); }); @@ -4086,15 +4087,13 @@ async fn test_collaborating_with_external_editorconfig( // Verify client A sees updated settings cx_a.read(|cx| { - let file = buffer_a.read(cx).file(); - let settings = language_settings(Some("Rust".into()), file, cx); + let settings = LanguageSettings::for_buffer(&buffer_a.read(cx), cx); assert_eq!(Some(settings.tab_size), NonZeroU32::new(9)); }); // Verify client B also sees updated settings cx_b.read(|cx| { - let file = buffer_b.read(cx).file(); - let settings = language_settings(Some("Rust".into()), file, cx); + let settings = LanguageSettings::for_buffer(&buffer_b.read(cx), cx); assert_eq!(Some(settings.tab_size), NonZeroU32::new(9)); }); } diff --git a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs index 4c4f37489608be0313921be13cd9b09d5bf77c6d..fe93a06f7265d102d8727466c46e83daf066e506 100644 --- a/crates/collab/tests/integration/remote_editing_collaboration_tests.rs +++ b/crates/collab/tests/integration/remote_editing_collaboration_tests.rs @@ -14,7 +14,7 @@ use gpui::{ use http_client::BlockedHttpClient; use language::{ FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry, - language_settings::{Formatter, FormatterList, language_settings}, + language_settings::{Formatter, FormatterList, LanguageSettings}, rust_lang, tree_sitter_typescript, }; use node_runtime::NodeRuntime; @@ -91,6 +91,7 @@ async fn test_sharing_an_ssh_remote_project( let remote_http_client = Arc::new(BlockedHttpClient); let node = NodeRuntime::unavailable(); let languages = Arc::new(LanguageRegistry::new(server_cx.executor())); + languages.add(rust_lang()); let _headless_project = server_cx.new(|cx| { HeadlessProject::new( HeadlessAppState { @@ -121,6 +122,7 @@ async fn test_sharing_an_ssh_remote_project( // User B joins the project. let project_b = client_b.join_remote_project(project_id, cx_b).await; + project_b.update(cx_b, |project, _| project.languages().add(rust_lang())); let worktree_b = project_b .update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx)) .unwrap(); @@ -173,9 +175,8 @@ async fn test_sharing_an_ssh_remote_project( executor.run_until_parked(); cx_b.read(|cx| { - let file = buffer_b.read(cx).file(); assert_eq!( - language_settings(Some("Rust".into()), file, cx).language_servers, + LanguageSettings::for_buffer(buffer_b.read(cx), cx).language_servers, ["override-rust-analyzer".to_string()] ) }); @@ -1284,9 +1285,8 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m let fake_language_server = fake_language_servers.next(); cx_a.read(|cx| { - let file = buffer_before_approval.read(cx).file(); assert_eq!( - language_settings(Some("Rust".into()), file, cx).language_servers, + LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers, ["...".to_string()], "remote .zed/settings.json must not sync before trust approval" ) @@ -1313,9 +1313,8 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m cx_a.run_until_parked(); cx_a.read(|cx| { - let file = buffer_before_approval.read(cx).file(); assert_eq!( - language_settings(Some("Rust".into()), file, cx).language_servers, + LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers, ["override-rust-analyzer".to_string()], "remote .zed/settings.json should sync after trust approval" ) diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 5b028671ed512a09cb90bcc4098d793a50b0fdb8..1ea974c4fe2ace4be4aeaf0064304a7a4ee2fb08 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1337,11 +1337,10 @@ impl PickerDelegate for DebugDelegate { else { return; }; - let file = location.buffer.read(cx).file(); - let language = location.buffer.read(cx).language(); - let language_name = language.as_ref().map(|l| l.name()); + let buffer = location.buffer.read(cx); + let language = buffer.language(); let Some(adapter): Option = - language::language_settings::language_settings(language_name, file, cx) + language::language_settings::LanguageSettings::for_buffer(buffer, cx) .debuggers .first() .map(SharedString::from) diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs index 2d50e7fa2321750634500925b0b6ec2b6989163d..d85ccded26058331c787f89ad74721d9572db623 100644 --- a/crates/edit_prediction_ui/src/edit_prediction_button.rs +++ b/crates/edit_prediction_ui/src/edit_prediction_button.rs @@ -18,7 +18,9 @@ use gpui::{ use indoc::indoc; use language::{ EditPredictionsMode, File, Language, - language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings}, + language_settings::{ + AllLanguageSettings, EditPredictionProvider, LanguageSettings, all_language_settings, + }, }; use project::{DisableAiSettings, Project}; use regex::Regex; @@ -674,8 +676,7 @@ impl EditPredictionButton { let language_state = self.language.as_ref().map(|language| { ( language.clone(), - language_settings::language_settings(Some(language.name()), None, cx) - .show_edit_predictions, + LanguageSettings::resolve(None, Some(&language.name()), cx).show_edit_predictions, ) }); @@ -1599,8 +1600,7 @@ fn emit_edit_prediction_menu_opened( ) { let language_name = language.as_ref().map(|l| l.name()); let edit_predictions_enabled_for_language = - language_settings::language_settings(language_name, file.as_ref(), cx) - .show_edit_predictions; + LanguageSettings::resolve(None, language_name.as_ref(), cx).show_edit_predictions; let file_extension = file .as_ref() .and_then(|f| { diff --git a/crates/editor/src/bracket_colorization.rs b/crates/editor/src/bracket_colorization.rs index ad2fc1bd8b9666dfa5e2c4b0367984c6398c98f8..5d0e7311a3d2908f498774fde81d660dd5450123 100644 --- a/crates/editor/src/bracket_colorization.rs +++ b/crates/editor/src/bracket_colorization.rs @@ -8,7 +8,7 @@ use crate::{Editor, HighlightKey}; use collections::{HashMap, HashSet}; use gpui::{AppContext as _, Context, HighlightStyle}; use itertools::Itertools; -use language::{BufferRow, BufferSnapshot, language_settings}; +use language::{BufferRow, BufferSnapshot, language_settings::LanguageSettings}; use multi_buffer::{Anchor, ExcerptId}; use ui::{ActiveTheme, utils::ensure_minimum_contrast}; @@ -29,14 +29,9 @@ impl Editor { let excerpt_data: Vec<(ExcerptId, BufferSnapshot, Range)> = visible_excerpts .into_iter() .filter_map(|(excerpt_id, (buffer, _, buffer_range))| { - let buffer_snapshot = buffer.read(cx).snapshot(); - if language_settings::language_settings( - buffer_snapshot.language().map(|language| language.name()), - buffer_snapshot.file(), - cx, - ) - .colorize_brackets - { + let buffer = buffer.read(cx); + let buffer_snapshot = buffer.snapshot(); + if LanguageSettings::for_buffer(&buffer, cx).colorize_brackets { Some((excerpt_id, buffer_snapshot, buffer_range)) } else { None diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index c5018abe598e7a20eaccf7c20f68a5f973f71436..e22cafb00bd446b94d3b8eda4d7e3afd20c449ae 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -97,7 +97,10 @@ use gpui::{ App, Context, Entity, EntityId, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle, WeakEntity, }; -use language::{Point, Subscription as BufferSubscription, language_settings::language_settings}; +use language::{ + Point, Subscription as BufferSubscription, + language_settings::{AllLanguageSettings, LanguageSettings}, +}; use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset, ToPoint, @@ -105,6 +108,7 @@ use multi_buffer::{ use project::project_settings::DiagnosticSeverity; use project::{InlayId, lsp_store::LspFoldingRange, lsp_store::TokenType}; use serde::Deserialize; +use settings::Settings; use smallvec::SmallVec; use sum_tree::{Bias, TreeMap}; use text::{BufferId, LineIndent, Patch}; @@ -1443,12 +1447,11 @@ impl DisplayMap { #[instrument(skip_all)] fn tab_size(buffer: &Entity, cx: &App) -> NonZeroU32 { - let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx)); - let language = buffer - .and_then(|buffer| buffer.language()) - .map(|l| l.name()); - let file = buffer.and_then(|buffer| buffer.file()); - language_settings(language, file, cx).tab_size + if let Some(buffer) = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx)) { + LanguageSettings::for_buffer(buffer, cx).tab_size + } else { + AllLanguageSettings::get_global(cx).defaults.tab_size + } } #[cfg(test)] @@ -1667,11 +1670,7 @@ impl DisplaySnapshot { else { return false; }; - let settings = language_settings( - buffer_snapshot.language().map(|l| l.name()), - buffer_snapshot.file(), - cx, - ); + let settings = LanguageSettings::for_buffer_snapshot(&buffer_snapshot, None, cx); settings.semantic_tokens.use_tree_sitter() } diff --git a/crates/editor/src/document_symbols.rs b/crates/editor/src/document_symbols.rs index 0228bbd917ad96b94778b2fc01d3a66e81224296..0668a034c8755a8702e31ec3a060b7f3b79c6829 100644 --- a/crates/editor/src/document_symbols.rs +++ b/crates/editor/src/document_symbols.rs @@ -5,7 +5,7 @@ use futures::FutureExt; use futures::future::join_all; use gpui::{App, Context, HighlightStyle, Task}; use itertools::Itertools as _; -use language::language_settings::language_settings; +use language::language_settings::LanguageSettings; use language::{Buffer, OutlineItem}; use multi_buffer::{ Anchor, AnchorRangeExt as _, MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot, @@ -239,7 +239,7 @@ impl Editor { } fn lsp_symbols_enabled(buffer: &Buffer, cx: &App) -> bool { - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + LanguageSettings::for_buffer(buffer, cx) .document_symbols .lsp_enabled() } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 9ab2549808124b4ce10fb7f711d2cf6ce293c279..ee659f2870502a96d1e052035d974b10213f5604 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -136,8 +136,8 @@ use language::{ OutlineItem, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, language_settings::{ - self, LanguageSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, - all_language_settings, language_settings, + self, AllLanguageSettings, LanguageSettings, LspInsertMode, RewrapBehavior, + WordsCompletionMode, all_language_settings, }, point_from_lsp, point_to_lsp, text_diff_with_options, }; @@ -596,7 +596,8 @@ impl Default for EditorStyle { } pub fn make_inlay_hints_style(cx: &App) -> HighlightStyle { - let show_background = language_settings::language_settings(None, None, cx) + let show_background = AllLanguageSettings::get_global(cx) + .defaults .inlay_hints .show_background; @@ -5977,14 +5978,7 @@ impl Editor { .read(cx) .text_anchor_for_position(position, cx)?; - let settings = language_settings::language_settings( - buffer - .read(cx) - .language_at(buffer_position) - .map(|l| l.name()), - buffer.read(cx).file(), - cx, - ); + let settings = LanguageSettings::for_buffer_at(&buffer.read(cx), buffer_position, cx); if !settings.use_on_type_format { return None; } @@ -6098,8 +6092,7 @@ impl Editor { let language = buffer_snapshot .language_at(buffer_position.text_anchor) .map(|language| language.name()); - - let language_settings = language_settings(language.clone(), buffer_snapshot.file(), cx); + let language_settings = multibuffer_snapshot.language_settings_at(buffer_position, cx); let completion_settings = language_settings.completions.clone(); let show_completions_on_input = self @@ -6967,8 +6960,7 @@ impl Editor { let resolved_tasks = resolved_tasks.as_ref()?; let buffer = buffer.read(cx); let language = buffer.language()?; - let file = buffer.file(); - let debug_adapter = language_settings(language.name().into(), file, cx) + let debug_adapter = LanguageSettings::for_buffer(&buffer, cx) .debuggers .first() .map(SharedString::from) @@ -8066,11 +8058,7 @@ impl Editor { return EditPredictionSettings::Disabled; } - let buffer = buffer.read(cx); - - let file = buffer.file(); - - if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions { + if !LanguageSettings::for_buffer(&buffer.read(cx), cx).show_edit_predictions { return EditPredictionSettings::Disabled; }; @@ -8085,6 +8073,7 @@ impl Editor { .as_ref() .is_some_and(|provider| provider.provider.show_predictions_in_menu()); + let file = buffer.read(cx).file(); let preview_requires_modifier = all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle; @@ -24484,9 +24473,8 @@ impl Editor { |mut acc, buffer| { let buffer = buffer.read(cx); let language = buffer.language().map(|language| language.name()); - if let hash_map::Entry::Vacant(v) = acc.entry(language.clone()) { - let file = buffer.file(); - v.insert(language_settings(language, file, cx).into_owned()); + if let hash_map::Entry::Vacant(v) = acc.entry(language) { + v.insert(LanguageSettings::for_buffer(&buffer, cx).into_owned()); } acc }, @@ -25988,10 +25976,9 @@ fn process_completion_for_edit( CompletionIntent::CompleteWithInsert => false, CompletionIntent::CompleteWithReplace => true, CompletionIntent::Complete | CompletionIntent::Compose => { - let insert_mode = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - .completions - .lsp_insert_mode; + let insert_mode = LanguageSettings::for_buffer(&buffer, cx) + .completions + .lsp_insert_mode; match insert_mode { LspInsertMode::Insert => false, LspInsertMode::Replace => true, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e8ae74c5a42b1021d8feae851ed8af4d67710a4c..f285d130be5be071b75161c114da53ca9c55d301 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -32725,10 +32725,12 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) { let fake_language_server = fake_language_servers.next(); cx.read(|cx| { - let file = buffer_before_approval.read(cx).file(); assert_eq!( - language::language_settings::language_settings(Some("Rust".into()), file, cx) - .language_servers, + language::language_settings::LanguageSettings::for_buffer( + buffer_before_approval.read(cx), + cx + ) + .language_servers, ["...".to_string()], "local .zed/settings.json must not apply before trust approval" ) @@ -32756,10 +32758,12 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) { cx.run_until_parked(); cx.read(|cx| { - let file = buffer_before_approval.read(cx).file(); assert_eq!( - language::language_settings::language_settings(Some("Rust".into()), file, cx) - .language_servers, + language::language_settings::LanguageSettings::for_buffer( + buffer_before_approval.read(cx), + cx + ) + .language_servers, ["override-rust-analyzer".to_string()], "local .zed/settings.json should apply after trust approval" ) diff --git a/crates/editor/src/folding_ranges.rs b/crates/editor/src/folding_ranges.rs index c4113f0504430b2926bb3c7858226afd2ff7bb40..de32f481d52e501eea8f7814f4b114fbdbbd0458 100644 --- a/crates/editor/src/folding_ranges.rs +++ b/crates/editor/src/folding_ranges.rs @@ -1,6 +1,6 @@ use futures::future::join_all; use itertools::Itertools; -use language::language_settings::language_settings; +use language::language_settings::LanguageSettings; use text::BufferId; use ui::{Context, Window}; @@ -29,13 +29,9 @@ impl Editor { let id = buffer.read(cx).remote_id(); (for_buffer.is_none_or(|target| target == id)) && self.registered_buffers.contains_key(&id) - && language_settings( - buffer.read(cx).language().map(|l| l.name()), - buffer.read(cx).file(), - cx, - ) - .document_folding_ranges - .enabled() + && LanguageSettings::for_buffer(buffer.read(cx), cx) + .document_folding_ranges + .enabled() }) .unique_by(|buffer| buffer.read(cx).remote_id()) .collect::>(); @@ -104,7 +100,7 @@ impl Editor { .into_iter() .filter(|buffer| { let buffer = buffer.read(cx); - !language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + !LanguageSettings::for_buffer(&buffer, cx) .document_folding_ranges .enabled() }) diff --git a/crates/editor/src/indent_guides.rs b/crates/editor/src/indent_guides.rs index 469099c6adf5b42f0d5e976abded4f7f3f639075..17ce3bfef9188912aba7e99644c5ca934fe32a71 100644 --- a/crates/editor/src/indent_guides.rs +++ b/crates/editor/src/indent_guides.rs @@ -2,7 +2,7 @@ use std::{cmp::Ordering, ops::Range, time::Duration}; use collections::HashSet; use gpui::{App, AppContext as _, Context, Task, Window}; -use language::language_settings::language_settings; +use language::language_settings::LanguageSettings; use multi_buffer::{IndentGuide, MultiBufferRow, ToPoint}; use text::{LineIndent, Point}; use util::ResultExt; @@ -37,13 +37,9 @@ impl Editor { ) -> Option> { let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| { if let Some(buffer) = self.buffer().read(cx).as_singleton() { - language_settings( - buffer.read(cx).language().map(|l| l.name()), - buffer.read(cx).file(), - cx, - ) - .indent_guides - .enabled + LanguageSettings::for_buffer(buffer.read(cx), cx) + .indent_guides + .enabled } else { true } diff --git a/crates/editor/src/inlays/inlay_hints.rs b/crates/editor/src/inlays/inlay_hints.rs index 414829dc3bbcd89f5f4e4337a955cfff5bb57fca..157de3c87d0c6a2f5fcde63ce89143fd8f2fb01b 100644 --- a/crates/editor/src/inlays/inlay_hints.rs +++ b/crates/editor/src/inlays/inlay_hints.rs @@ -11,7 +11,7 @@ use gpui::{App, Entity, Pixels, Task}; use itertools::Itertools; use language::{ BufferRow, - language_settings::{InlayHintKind, InlayHintSettings, language_settings}, + language_settings::{InlayHintKind, InlayHintSettings}, }; use lsp::LanguageServerId; use multi_buffer::{Anchor, ExcerptId, MultiBufferSnapshot}; @@ -38,9 +38,7 @@ pub fn inlay_hint_settings( snapshot: &MultiBufferSnapshot, cx: &mut Context, ) -> InlayHintSettings { - let file = snapshot.file_at(location); - let language = snapshot.language_at(location).map(|l| l.name()); - language_settings(language, file, cx).inlay_hints + snapshot.language_settings_at(location, cx).inlay_hints } #[derive(Debug)] diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index a7c0c5eed2aed44d69bcaa3657894bad4d9deeb1..b91f039aff7cfb8bc7997cfbf63abb8dbe4662e5 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -5,7 +5,7 @@ use multi_buffer::{BufferOffset, MultiBuffer, ToOffset}; use std::ops::Range; use util::ResultExt as _; -use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node}; +use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node, language_settings::LanguageSettings}; use text::{Anchor, OffsetRangeExt as _}; use crate::{Editor, SelectionEffects}; @@ -322,12 +322,10 @@ pub(crate) fn refresh_enabled_in_any_buffer( if language.config().jsx_tag_auto_close.is_none() { continue; } - let language_settings = language::language_settings::language_settings( - Some(language.name()), - snapshot.file(), - cx, - ); - if language_settings.jsx_tag_auto_close { + let should_auto_close = + LanguageSettings::resolve(Some(buffer), Some(&language.name()), cx) + .jsx_tag_auto_close; + if should_auto_close { found_enabled = true; } } diff --git a/crates/editor/src/runnables.rs b/crates/editor/src/runnables.rs index fb234f9bcf9976fa71ac6cac055d3312f96a3d9a..92663ff9a96d1f84e2de387917e2d6a32b16aa00 100644 --- a/crates/editor/src/runnables.rs +++ b/crates/editor/src/runnables.rs @@ -637,17 +637,17 @@ impl Editor { runnable: &mut Runnable, cx: &mut App, ) -> Task> { - let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { - let (worktree_id, file) = project - .buffer_for_id(runnable.buffer, cx) + let (inventory, worktree_id, buffer) = project.read_with(cx, |project, cx| { + let buffer = project.buffer_for_id(runnable.buffer, cx); + let worktree_id = buffer + .as_ref() .and_then(|buffer| buffer.read(cx).file()) - .map(|file| (file.worktree_id(cx), file.clone())) - .unzip(); + .map(|file| file.worktree_id(cx)); ( project.task_store().read(cx).task_inventory().cloned(), worktree_id, - file, + buffer, ) }); @@ -658,7 +658,12 @@ impl Editor { if let Some(inventory) = inventory { for RunnableTag(tag) in tags { let new_tasks = inventory.update(cx, |inventory, cx| { - inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx) + inventory.list_tasks( + buffer.clone(), + Some(language.clone()), + worktree_id, + cx, + ) }); templates_with_tags.extend(new_tasks.await.into_iter().filter( move |(_, template)| { @@ -715,7 +720,7 @@ mod tests { use std::{sync::Arc, time::Duration}; use futures::StreamExt as _; - use gpui::{AppContext as _, Task, TestAppContext}; + use gpui::{AppContext as _, Entity, Task, TestAppContext}; use indoc::indoc; use language::{ContextProvider, FakeLspAdapter}; use languages::rust_lang; @@ -742,7 +747,7 @@ mod tests { impl ContextProvider for TestRustContextProvider { fn associated_tasks( &self, - _: Option>, + _: Option>, _: &gpui::App, ) -> Task> { Task::ready(Some(TaskTemplates(vec![ @@ -769,7 +774,7 @@ mod tests { impl ContextProvider for TestRustContextProviderWithLsp { fn associated_tasks( &self, - _: Option>, + _: Option>, _: &gpui::App, ) -> Task> { Task::ready(Some(TaskTemplates(vec![TaskTemplate { diff --git a/crates/editor/src/semantic_tokens.rs b/crates/editor/src/semantic_tokens.rs index e95b20aed5a6655d6ae4ccd2c6658cfcfecc2ea4..6a82068410f074c3246f2d84eab9a3576f2e8848 100644 --- a/crates/editor/src/semantic_tokens.rs +++ b/crates/editor/src/semantic_tokens.rs @@ -6,7 +6,7 @@ use gpui::{ App, Context, FontStyle, FontWeight, HighlightStyle, StrikethroughStyle, Task, UnderlineStyle, }; use itertools::Itertools; -use language::language_settings::language_settings; +use language::language_settings::LanguageSettings; use project::{ lsp_store::{ BufferSemanticToken, BufferSemanticTokens, RefreshForServer, SemanticTokenStylizer, @@ -155,13 +155,9 @@ impl Editor { .filter_map(|editor_buffer| { let editor_buffer_id = editor_buffer.read(cx).remote_id(); if self.registered_buffers.contains_key(&editor_buffer_id) - && language_settings( - editor_buffer.read(cx).language().map(|l| l.name()), - editor_buffer.read(cx).file(), - cx, - ) - .semantic_tokens - .enabled() + && LanguageSettings::for_buffer(editor_buffer.read(cx), cx) + .semantic_tokens + .enabled() { Some((editor_buffer_id, editor_buffer)) } else { @@ -184,7 +180,7 @@ impl Editor { .buffer(*buffer_id) .is_some_and(|buffer| { let buffer = buffer.read(cx); - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) + LanguageSettings::for_buffer(&buffer, cx) .semantic_tokens .enabled() }) diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs index fa93709d077d435e9d6b579ece8890885675329d..f1a209ca7af19589e897c42e9f5269abaa42725a 100644 --- a/crates/extension_host/src/extension_store_test.rs +++ b/crates/extension_host/src/extension_store_test.rs @@ -216,6 +216,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { matcher: LanguageMatcher { path_suffixes: vec!["erb".into()], first_line_pattern: None, + ..LanguageMatcher::default() }, }, ), @@ -229,6 +230,7 @@ async fn test_extension_store(cx: &mut TestAppContext) { matcher: LanguageMatcher { path_suffixes: vec!["rb".into()], first_line_pattern: None, + ..LanguageMatcher::default() }, }, ), diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8a3886a7832fabbd67340f7f6d19b36557aa24a8..41aaf5b162d9fc231d5a5f37d20b9b79cf23564b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,10 +1,10 @@ pub mod row_chunk; use crate::{ - DebuggerTextObject, LanguageScope, Outline, OutlineConfig, PLAIN_TEXT, RunnableCapture, - RunnableTag, TextObject, TreeSitterOptions, + DebuggerTextObject, LanguageScope, ModelineSettings, Outline, OutlineConfig, PLAIN_TEXT, + RunnableCapture, RunnableTag, TextObject, TreeSitterOptions, diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup}, - language_settings::{AutoIndentMode, LanguageSettings, language_settings}, + language_settings::{AutoIndentMode, LanguageSettings}, outline::OutlineItem, row_chunk::RowChunks, syntax_map::{ @@ -135,6 +135,7 @@ pub struct Buffer { /// The contents of a cell are (self.version, has_changes) at the time of a last call. has_unsaved_edits: Cell<(clock::Global, bool)>, change_bits: Vec>>, + modeline: Option>, _subscriptions: Vec, tree_sitter_data: Arc, encoding: &'static Encoding, @@ -195,6 +196,7 @@ pub struct BufferSnapshot { file: Option>, non_text_state_update_count: usize, pub capability: Capability, + modeline: Option>, } /// The kind and amount of indentation in a particular line. For now, @@ -1163,6 +1165,7 @@ impl Buffer { deferred_ops: OperationQueue::new(), has_conflict: false, change_bits: Default::default(), + modeline: None, _subscriptions: Vec::new(), encoding: encoding_rs::UTF_8, has_bom: false, @@ -1175,6 +1178,7 @@ impl Buffer { text: Rope, language: Option>, language_registry: Option>, + modeline: Option>, cx: &mut App, ) -> impl Future + use<> { let entity_id = cx.reserve_entity::().entity_id(); @@ -1199,6 +1203,7 @@ impl Buffer { language, non_text_state_update_count: 0, capability: Capability::ReadOnly, + modeline, } } } @@ -1225,6 +1230,7 @@ impl Buffer { language: None, non_text_state_update_count: 0, capability: Capability::ReadOnly, + modeline: None, } } @@ -1255,6 +1261,7 @@ impl Buffer { language, non_text_state_update_count: 0, capability: Capability::ReadOnly, + modeline: None, } } @@ -1285,6 +1292,7 @@ impl Buffer { language: self.language.clone(), non_text_state_update_count: self.non_text_state_update_count, capability: self.capability, + modeline: self.modeline.clone(), } } @@ -1537,6 +1545,21 @@ impl Buffer { ); } + /// Assign the buffer [`ModelineSettings`]. + pub fn set_modeline(&mut self, modeline: Option) -> bool { + if modeline.as_ref() != self.modeline.as_deref() { + self.modeline = modeline.map(Arc::new); + true + } else { + false + } + } + + /// Returns the [`ModelineSettings`]. + pub fn modeline(&self) -> Option<&Arc> { + self.modeline.as_ref() + } + /// Assign the buffer a new [`Capability`]. pub fn set_capability(&mut self, capability: Capability, cx: &mut Context) { if self.capability != capability { @@ -2755,8 +2778,12 @@ impl Buffer { } else { // The auto-indent setting is not present in editorconfigs, hence // we can avoid passing the file here. - let auto_indent_mode = - language_settings(language.map(|l| l.name()), None, cx).auto_indent; + let auto_indent_mode = LanguageSettings::resolve( + None, + language.map(|l| l.name()).as_ref(), + cx, + ) + .auto_indent; let apply_syntax_indent = auto_indent_mode == AutoIndentMode::SyntaxAware; previous_setting = Some((language_id, apply_syntax_indent)); apply_syntax_indent @@ -3397,11 +3424,7 @@ impl BufferSnapshot { /// Returns [`IndentSize`] for a given position that respects user settings /// and language preferences. pub fn language_indent_size_at(&self, position: T, cx: &App) -> IndentSize { - let settings = language_settings( - self.language_at(position).map(|l| l.name()), - self.file(), - cx, - ); + let settings = self.settings_at(position, cx); if settings.hard_tabs { IndentSize::tab() } else { @@ -3867,6 +3890,11 @@ impl BufferSnapshot { }) } + /// Returns the [`ModelineSettings`]. + pub fn modeline(&self) -> Option<&Arc> { + self.modeline.as_ref() + } + /// Returns the main [`Language`]. pub fn language(&self) -> Option<&Arc> { self.language.as_ref() @@ -3885,11 +3913,7 @@ impl BufferSnapshot { position: D, cx: &'a App, ) -> Cow<'a, LanguageSettings> { - language_settings( - self.language_at(position).map(|l| l.name()), - self.file.as_ref(), - cx, - ) + LanguageSettings::for_buffer_snapshot(self, Some(position.to_offset(self)), cx) } pub fn char_classifier_at(&self, point: T) -> CharClassifier { @@ -5511,6 +5535,7 @@ impl Clone for BufferSnapshot { tree_sitter_data: self.tree_sitter_data.clone(), non_text_state_update_count: self.non_text_state_update_count, capability: self.capability, + modeline: self.modeline.clone(), } } } diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index a47578faa2037e5f17a0e2be4ce5329e61d0fa84..9839c82e358682fdabcae95fea426d3b2d564969 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -246,6 +246,7 @@ async fn test_first_line_pattern(cx: &mut TestAppContext) { matcher: LanguageMatcher { path_suffixes: vec!["js".into()], first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()), + ..LanguageMatcher::default() }, ..Default::default() }); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index bdee0d9dd68c96eab47278a01f3e475548c16336..d9c1256290a0b47a198b700c2a5710c0b1df1795 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -12,6 +12,7 @@ mod highlight_map; mod language_registry; pub mod language_settings; mod manifest; +pub mod modeline; mod outline; pub mod proto; mod syntax_map; @@ -40,6 +41,7 @@ use lsp::{ CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions, Uri, }; pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery}; +pub use modeline::{ModelineSettings, parse_modeline}; use parking_lot::Mutex; use regex::Regex; use schemars::{JsonSchema, SchemaGenerator, json_schema}; @@ -138,6 +140,7 @@ pub static PLAIN_TEXT: LazyLock> = LazyLock::new(|| { matcher: LanguageMatcher { path_suffixes: vec!["txt".to_owned()], first_line_pattern: None, + modeline_aliases: vec!["text".to_owned(), "txt".to_owned()], }, brackets: BracketPairConfig { pairs: vec![ @@ -1010,6 +1013,11 @@ pub struct LanguageMatcher { )] #[schemars(schema_with = "regex_json_schema")] pub first_line_pattern: Option, + /// Alternative names for this language used in vim/emacs modelines. + /// These are matched case-insensitively against the `mode` (emacs) or + /// `filetype`/`ft` (vim) specified in the modeline. + #[serde(default)] + pub modeline_aliases: Vec, } /// The configuration for JSX tag auto-closing. diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index d73a44fda3347ebcec9c6798325838acec543566..0e13935aa42ac93d284b4606e65337158f18e1d0 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -745,6 +745,44 @@ impl LanguageRegistry { .cloned() } + /// Look up a language by its modeline name (vim filetype or emacs mode). + /// + /// This performs a case-insensitive match against: + /// 1. Explicit modeline aliases defined in the language config + /// 2. The language's grammar name + /// 3. The language name itself + pub fn available_language_for_modeline_name( + self: &Arc, + modeline_name: &str, + ) -> Option { + let modeline_name_lower = modeline_name.to_lowercase(); + let state = self.state.read(); + + state + .available_languages + .iter() + .find(|lang| { + lang.matcher + .modeline_aliases + .iter() + .any(|alias| alias.to_lowercase() == modeline_name_lower) + }) + .or_else(|| { + state.available_languages.iter().find(|lang| { + lang.grammar + .as_ref() + .is_some_and(|g| g.to_lowercase() == modeline_name_lower) + }) + }) + .or_else(|| { + state + .available_languages + .iter() + .find(|lang| lang.name.0.to_lowercase() == modeline_name_lower) + }) + .cloned() + } + pub fn language_for_file( self: &Arc, file: &Arc, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index f2c55fd1e8a3b8bf5b6c2dd8ea24d1343385fa78..5f74247ab04758e18dd38edc54929f85403d9e97 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -1,6 +1,8 @@ //! Provides `language`-related settings. -use crate::{File, Language, LanguageName, LanguageServerName}; +use crate::{ + Buffer, BufferSnapshot, File, Language, LanguageName, LanguageServerName, ModelineSettings, +}; use collections::{FxHashMap, HashMap, HashSet}; use ec4rs::{ Properties as EditorconfigProperties, @@ -17,22 +19,10 @@ pub use settings::{ LanguageSettingsContent, LspInsertMode, RewrapBehavior, ShowWhitespaceSetting, SoftWrap, WordsCompletionMode, }; -use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore}; +use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom}; use shellexpand; use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc}; - -/// Returns the settings for the specified language from the provided file. -pub fn language_settings<'a>( - language: Option, - file: Option<&'a Arc>, - cx: &'a App, -) -> Cow<'a, LanguageSettings> { - let location = file.map(|f| SettingsLocation { - worktree_id: f.worktree_id(cx), - path: f.path().as_ref(), - }); - AllLanguageSettings::get(location, cx).language(location, language.as_ref(), cx) -} +use text::ToOffset; /// Returns the settings for all languages from the provided file. pub fn all_language_settings<'a>( @@ -284,6 +274,74 @@ impl LanguageSettings { /// A token representing the rest of the available language servers. const REST_OF_LANGUAGE_SERVERS: &'static str = "..."; + pub fn for_buffer<'a>(buffer: &'a Buffer, cx: &'a App) -> Cow<'a, LanguageSettings> { + Self::resolve(Some(buffer), None, cx) + } + + pub fn for_buffer_at<'a, D: ToOffset>( + buffer: &'a Buffer, + position: D, + cx: &'a App, + ) -> Cow<'a, LanguageSettings> { + let language = buffer.language_at(position); + Self::resolve(Some(buffer), language.map(|l| l.name()).as_ref(), cx) + } + + pub fn for_buffer_snapshot<'a>( + buffer: &'a BufferSnapshot, + offset: Option, + cx: &'a App, + ) -> Cow<'a, LanguageSettings> { + let location = buffer.file().map(|f| SettingsLocation { + worktree_id: f.worktree_id(cx), + path: f.path().as_ref(), + }); + + let language = if let Some(offset) = offset { + buffer.language_at(offset) + } else { + buffer.language() + }; + + let mut settings = AllLanguageSettings::get(location, cx).language( + location, + language.map(|l| l.name()).as_ref(), + cx, + ); + + if let Some(modeline) = buffer.modeline() { + merge_with_modeline(settings.to_mut(), modeline); + } + + settings + } + + pub fn resolve<'a>( + buffer: Option<&'a Buffer>, + override_language: Option<&LanguageName>, + cx: &'a App, + ) -> Cow<'a, LanguageSettings> { + let Some(buffer) = buffer else { + return AllLanguageSettings::get(None, cx).language(None, override_language, cx); + }; + let location = buffer.file().map(|f| SettingsLocation { + worktree_id: f.worktree_id(cx), + path: f.path().as_ref(), + }); + let all = AllLanguageSettings::get(location, cx); + let mut settings = if override_language.is_none() { + all.language(location, buffer.language().map(|l| l.name()).as_ref(), cx) + } else { + all.language(location, override_language, cx) + }; + + if let Some(modeline) = buffer.modeline() { + merge_with_modeline(settings.to_mut(), modeline); + } + + settings + } + /// Returns the customized list of language servers from the list of /// available language servers. pub fn customized_language_servers( @@ -530,6 +588,42 @@ impl AllLanguageSettings { } } +fn merge_with_modeline(settings: &mut LanguageSettings, modeline: &ModelineSettings) { + let show_whitespaces = modeline.show_trailing_whitespace.and_then(|v| { + if v { + Some(ShowWhitespaceSetting::Trailing) + } else { + None + } + }); + + settings + .tab_size + .merge_from_option(modeline.tab_size.as_ref()); + settings + .hard_tabs + .merge_from_option(modeline.hard_tabs.as_ref()); + settings + .preferred_line_length + .merge_from_option(modeline.preferred_line_length.map(u32::from).as_ref()); + let auto_indent_mode = modeline.auto_indent.map(|enabled| { + if enabled { + AutoIndentMode::SyntaxAware + } else { + AutoIndentMode::None + } + }); + settings + .auto_indent + .merge_from_option(auto_indent_mode.as_ref()); + settings + .show_whitespaces + .merge_from_option(show_whitespaces.as_ref()); + settings + .ensure_final_newline_on_save + .merge_from_option(modeline.ensure_final_newline.as_ref()); +} + fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) { let preferred_line_length = cfg.get::().ok().and_then(|v| match v { MaxLineLen::Value(u) => Some(u as u32), @@ -557,22 +651,18 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr TrimTrailingWs::Value(b) => b, }) .ok(); - fn merge(target: &mut T, value: Option) { - if let Some(value) = value { - *target = value; - } - } - merge(&mut settings.preferred_line_length, preferred_line_length); - merge(&mut settings.tab_size, tab_size); - merge(&mut settings.hard_tabs, hard_tabs); - merge( - &mut settings.remove_trailing_whitespace_on_save, - remove_trailing_whitespace_on_save, - ); - merge( - &mut settings.ensure_final_newline_on_save, - ensure_final_newline_on_save, - ); + + settings + .preferred_line_length + .merge_from_option(preferred_line_length.as_ref()); + settings.tab_size.merge_from_option(tab_size.as_ref()); + settings.hard_tabs.merge_from_option(hard_tabs.as_ref()); + settings + .remove_trailing_whitespace_on_save + .merge_from_option(remove_trailing_whitespace_on_save.as_ref()); + settings + .ensure_final_newline_on_save + .merge_from_option(ensure_final_newline_on_save.as_ref()); } impl settings::Settings for AllLanguageSettings { diff --git a/crates/language/src/modeline.rs b/crates/language/src/modeline.rs new file mode 100644 index 0000000000000000000000000000000000000000..8b7e6044492fc64c5291a9d034466e1ec42a5ead --- /dev/null +++ b/crates/language/src/modeline.rs @@ -0,0 +1,763 @@ +use regex::Regex; +use std::{num::NonZeroU32, sync::LazyLock}; + +/// The settings extracted from an emacs/vim modelines. +/// +/// The parsing tries to best match the modeline directives and +/// variables to Zed, matching LanguageSettings fields. +/// The mode mapping is done later thanks to the LanguageRegistry. +/// +/// It is not exhaustive, but covers the most common settings. +#[derive(Debug, Clone, Default, PartialEq)] +pub struct ModelineSettings { + /// The emacs mode or vim filetype. + pub mode: Option, + /// How many columns a tab should occupy. + pub tab_size: Option, + /// Whether to indent lines using tab characters, as opposed to multiple + /// spaces. + pub hard_tabs: Option, + /// The number of bytes that comprise the indentation. + pub indent_size: Option, + /// Whether to auto-indent lines. + pub auto_indent: Option, + /// The column at which to soft-wrap lines. + pub preferred_line_length: Option, + /// Whether to ensure a final newline at the end of the file. + pub ensure_final_newline: Option, + /// Whether to show trailing whitespace on the editor. + pub show_trailing_whitespace: Option, + + /// Emacs modeline variables that were parsed but not mapped to Zed settings. + /// Stored as (variable-name, value) pairs. + pub emacs_extra_variables: Vec<(String, String)>, + /// Vim modeline options that were parsed but not mapped to Zed settings. + /// Stored as (option-name, value) pairs. + pub vim_extra_variables: Vec<(String, Option)>, +} + +impl ModelineSettings { + fn has_settings(&self) -> bool { + self != &Self::default() + } +} + +/// Parse modelines from file content. +/// +/// Supports: +/// - Emacs modelines: -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*- and "Local Variables" +/// - Vim modelines: vim: set ft=rust ts=4 sw=4 et: +pub fn parse_modeline(first_lines: &[&str], last_lines: &[&str]) -> Option { + let mut settings = ModelineSettings::default(); + + parse_modelines(first_lines, &mut settings); + + // Parse Emacs Local Variables in last lines + parse_emacs_local_variables(last_lines, &mut settings); + + // Also check for vim modelines in last lines if we don't have settings yet + if !settings.has_settings() { + parse_vim_modelines(last_lines, &mut settings); + } + + Some(settings).filter(|s| s.has_settings()) +} + +fn parse_modelines(modelines: &[&str], settings: &mut ModelineSettings) { + for line in modelines { + parse_emacs_modeline(line, settings); + // if emacs is set, do not check for vim modelines + if settings.has_settings() { + return; + } + } + + parse_vim_modelines(modelines, settings); +} + +static EMACS_MODELINE_RE: LazyLock = + LazyLock::new(|| Regex::new(r"-\*-\s*(.+?)\s*-\*-").expect("valid regex")); + +/// Parse Emacs-style modelines +/// Format: -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*- +/// See Emacs (set-auto-mode) +fn parse_emacs_modeline(line: &str, settings: &mut ModelineSettings) { + let Some(captures) = EMACS_MODELINE_RE.captures(line) else { + return; + }; + let Some(modeline_content) = captures.get(1).map(|m| m.as_str()) else { + return; + }; + for part in modeline_content.split(';') { + parse_emacs_key_value(part, settings, true); + } +} + +/// Parse Emacs-style Local Variables block +/// +/// Emacs supports a "Local Variables" block at the end of files: +/// ```text +/// /* Local Variables: */ +/// /* mode: c */ +/// /* tab-width: 4 */ +/// /* End: */ +/// ``` +/// +/// Emacs related code is hack-local-variables--find-variables in +/// https://cgit.git.savannah.gnu.org/cgit/emacs.git/tree/lisp/files.el#n4346 +fn parse_emacs_local_variables(lines: &[&str], settings: &mut ModelineSettings) { + const LOCAL_VARIABLES: &str = "Local Variables:"; + + let Some((start_idx, prefix, suffix)) = lines.iter().enumerate().find_map(|(i, line)| { + let prefix_len = line.find(LOCAL_VARIABLES)?; + let suffix_start = prefix_len + LOCAL_VARIABLES.len(); + Some((i, line.get(..prefix_len)?, line.get(suffix_start..)?)) + }) else { + return; + }; + + let mut continuation = String::new(); + + for line in &lines[start_idx + 1..] { + let Some(content) = line + .strip_prefix(prefix) + .and_then(|l| l.strip_suffix(suffix)) + .map(str::trim) + else { + return; + }; + + if let Some(continued) = content.strip_suffix('\\') { + continuation.push_str(continued); + continue; + } + + let to_parse = if continuation.is_empty() { + content + } else { + continuation.push_str(content); + &continuation + }; + + if to_parse == "End:" { + return; + } + + parse_emacs_key_value(to_parse, settings, false); + continuation.clear(); + } +} + +fn parse_emacs_key_value(part: &str, settings: &mut ModelineSettings, bare: bool) { + let part = part.trim(); + if part.is_empty() { + return; + } + + if let Some((key, value)) = part.split_once(':') { + let key = key.trim(); + let value = value.trim(); + + match key.to_lowercase().as_str() { + "mode" => { + settings.mode = Some(value.to_string()); + } + "c-basic-offset" | "python-indent-offset" => { + if let Ok(size) = value.parse::() { + settings.indent_size = Some(size); + } + } + "fill-column" => { + if let Ok(size) = value.parse::() { + settings.preferred_line_length = Some(size); + } + } + "tab-width" => { + if let Ok(size) = value.parse::() { + settings.tab_size = Some(size); + } + } + "indent-tabs-mode" => { + settings.hard_tabs = Some(value != "nil"); + } + "electric-indent-mode" => { + settings.auto_indent = Some(value != "nil"); + } + "require-final-newline" => { + settings.ensure_final_newline = Some(value != "nil"); + } + "show-trailing-whitespace" => { + settings.show_trailing_whitespace = Some(value != "nil"); + } + key => settings + .emacs_extra_variables + .push((key.to_string(), value.to_string())), + } + } else if bare { + // Handle bare mode specification (e.g., -*- rust -*-) + settings.mode = Some(part.to_string()); + } +} + +fn parse_vim_modelines(modelines: &[&str], settings: &mut ModelineSettings) { + for line in modelines { + parse_vim_modeline(line, settings); + } +} + +static VIM_MODELINE_PATTERNS: LazyLock> = LazyLock::new(|| { + [ + // Second form: [text{white}]{vi:vim:Vim:}[white]se[t] {options}:[text] + // Allow escaped colons in options: match non-colon chars or backslash followed by any char + r"(?:^|\s)(vi|vim|Vim):(?:\s*)se(?:t)?\s+((?:[^\\:]|\\.)*):", + // First form: [text{white}]{vi:vim:}[white]{options} + r"(?:^|\s+)(vi|vim):(?:\s*(.+))", + ] + .iter() + .map(|pattern| Regex::new(pattern).expect("valid regex")) + .collect() +}); + +/// Parse Vim-style modelines +/// Supports both forms: +/// 1. First form: vi:noai:sw=3 ts=6 +/// 2. Second form: vim: set ft=rust ts=4 sw=4 et: +fn parse_vim_modeline(line: &str, settings: &mut ModelineSettings) { + for re in VIM_MODELINE_PATTERNS.iter() { + if let Some(captures) = re.captures(line) { + if let Some(options) = captures.get(2) { + parse_vim_settings(options.as_str().trim(), settings); + break; + } + } + } +} + +fn parse_vim_settings(content: &str, settings: &mut ModelineSettings) { + fn split_colon_unescape(input: &str) -> Vec { + let mut split = Vec::new(); + let mut str = String::new(); + let mut chars = input.chars().peekable(); + while let Some(c) = chars.next() { + if c == '\\' { + match chars.next() { + Some(escaped_char) => str.push(escaped_char), + None => str.push('\\'), + } + } else if c == ':' { + split.push(std::mem::take(&mut str)); + } else { + str.push(c); + } + } + split.push(str); + split + } + + let parts = split_colon_unescape(content); + for colon_part in parts { + let colon_part = colon_part.trim(); + if colon_part.is_empty() { + continue; + } + + // Each colon part might contain space-separated options + for part in colon_part.split_whitespace() { + if let Some((key, value)) = part.split_once('=') { + match key { + "ft" | "filetype" => { + settings.mode = Some(value.to_string()); + } + "ts" | "tabstop" => { + if let Ok(size) = value.parse::() { + settings.tab_size = Some(size); + } + } + "sw" | "shiftwidth" => { + if let Ok(size) = value.parse::() { + settings.indent_size = Some(size); + } + } + "tw" | "textwidth" => { + if let Ok(size) = value.parse::() { + settings.preferred_line_length = Some(size); + } + } + _ => { + settings + .vim_extra_variables + .push((key.to_string(), Some(value.to_string()))); + } + } + } else { + match part { + "ai" | "autoindent" => { + settings.auto_indent = Some(true); + } + "noai" | "noautoindent" => { + settings.auto_indent = Some(false); + } + "et" | "expandtab" => { + settings.hard_tabs = Some(false); + } + "noet" | "noexpandtab" => { + settings.hard_tabs = Some(true); + } + "eol" | "endofline" => { + settings.ensure_final_newline = Some(true); + } + "noeol" | "noendofline" => { + settings.ensure_final_newline = Some(false); + } + "set" => { + // Ignore the "set" keyword itself + } + _ => { + settings.vim_extra_variables.push((part.to_string(), None)); + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use indoc::indoc; + use pretty_assertions::assert_eq; + + #[test] + fn test_no_modeline() { + let content = "This is just regular content\nwith no modeline"; + assert!(parse_modeline(&[content], &[content]).is_none()); + } + + #[test] + fn test_emacs_bare_mode() { + let content = "/* -*- rust -*- */"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("rust".to_string()), + ..Default::default() + } + ); + } + + #[test] + fn test_emacs_modeline_parsing() { + let content = "/* -*- mode: rust; tab-width: 4; indent-tabs-mode: nil; -*- */"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("rust".to_string()), + tab_size: Some(NonZeroU32::new(4).unwrap()), + hard_tabs: Some(false), + ..Default::default() + } + ); + } + + #[test] + fn test_emacs_last_line_parsing() { + let content = indoc! {r#" + # Local Variables: + # compile-command: "cc foo.c -Dfoo=bar -Dhack=whatever \ + # -Dmumble=blaah" + # End: + "#} + .lines() + .collect::>(); + let settings = parse_modeline(&[], &content).unwrap(); + assert_eq!( + settings, + ModelineSettings { + emacs_extra_variables: vec![( + "compile-command".to_string(), + "\"cc foo.c -Dfoo=bar -Dhack=whatever -Dmumble=blaah\"".to_string() + ),], + ..Default::default() + } + ); + + let content = indoc! {" + foo + /* Local Variables: */ + /* eval: (font-lock-mode -1) */ + /* mode: old-c */ + /* mode: c */ + /* End: */ + /* mode: ignored */ + "} + .lines() + .collect::>(); + let settings = parse_modeline(&[], &content).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("c".to_string()), + emacs_extra_variables: vec![( + "eval".to_string(), + "(font-lock-mode -1)".to_string() + ),], + ..Default::default() + } + ); + } + + #[test] + fn test_vim_modeline_parsing() { + // Test second form (set format) + let content = "// vim: set ft=rust ts=4 sw=4 et:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("rust".to_string()), + tab_size: Some(NonZeroU32::new(4).unwrap()), + hard_tabs: Some(false), + indent_size: Some(NonZeroU32::new(4).unwrap()), + ..Default::default() + } + ); + + // Test first form (colon-separated) + let content = "vi:noai:sw=3:ts=6"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + tab_size: Some(NonZeroU32::new(6).unwrap()), + auto_indent: Some(false), + indent_size: Some(NonZeroU32::new(3).unwrap()), + ..Default::default() + } + ); + } + + #[test] + fn test_vim_modeline_first_form() { + // Examples from vim specification: vi:noai:sw=3 ts=6 + let content = " vi:noai:sw=3 ts=6 "; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + tab_size: Some(NonZeroU32::new(6).unwrap()), + auto_indent: Some(false), + indent_size: Some(NonZeroU32::new(3).unwrap()), + ..Default::default() + } + ); + + // Test with filetype + let content = "vim:ft=python:ts=8:noet"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("python".to_string()), + tab_size: Some(NonZeroU32::new(8).unwrap()), + hard_tabs: Some(true), + ..Default::default() + } + ); + } + + #[test] + fn test_vim_modeline_second_form() { + // Examples from vim specification: /* vim: set ai tw=75: */ + let content = "/* vim: set ai tw=75: */"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + auto_indent: Some(true), + preferred_line_length: Some(NonZeroU32::new(75).unwrap()), + ..Default::default() + } + ); + + // Test with 'Vim:' (capital V) + let content = "/* Vim: set ai tw=75: */"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + auto_indent: Some(true), + preferred_line_length: Some(NonZeroU32::new(75).unwrap()), + ..Default::default() + } + ); + + // Test 'se' shorthand + let content = "// vi: se ft=c ts=4:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("c".to_string()), + tab_size: Some(NonZeroU32::new(4).unwrap()), + ..Default::default() + } + ); + + // Test complex modeline with encoding + let content = "# vim: set ft=python ts=4 sw=4 et encoding=utf-8:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("python".to_string()), + tab_size: Some(NonZeroU32::new(4).unwrap()), + hard_tabs: Some(false), + indent_size: Some(NonZeroU32::new(4).unwrap()), + vim_extra_variables: vec![("encoding".to_string(), Some("utf-8".to_string()))], + ..Default::default() + } + ); + } + + #[test] + fn test_vim_modeline_edge_cases() { + // Test modeline at start of line (compatibility with version 3.0) + let content = "vi:ts=2:et"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + tab_size: Some(NonZeroU32::new(2).unwrap()), + hard_tabs: Some(false), + ..Default::default() + } + ); + + // Test vim at start of line + let content = "vim:ft=rust:noet"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("rust".to_string()), + hard_tabs: Some(true), + ..Default::default() + } + ); + + // Test mixed boolean flags + let content = "vim: set wrap noet ts=8:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + tab_size: Some(NonZeroU32::new(8).unwrap()), + hard_tabs: Some(true), + vim_extra_variables: vec![("wrap".to_string(), None)], + ..Default::default() + } + ); + } + + #[test] + fn test_vim_modeline_invalid_cases() { + // Test malformed options are ignored gracefully + let content = "vim: set ts=invalid ft=rust:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!( + settings, + ModelineSettings { + mode: Some("rust".to_string()), + ..Default::default() + } + ); + + // Test empty modeline content - this should still work as there might be options + let content = "vim: set :"; + // This should return None because there are no actual options + let result = parse_modeline(&[content], &[]); + assert!(result.is_none(), "Expected None but got: {:?}", result); + + // Test modeline without proper format + let content = "not a modeline"; + assert!(parse_modeline(&[content], &[]).is_none()); + + // Test word that looks like modeline but isn't + let content = "example: this could be confused with ex:"; + assert!(parse_modeline(&[content], &[]).is_none()); + } + + #[test] + fn test_vim_language_mapping() { + // Test vim-specific language mappings + let content = "vim: set ft=sh:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!(settings.mode, Some("sh".to_string())); + + let content = "vim: set ft=golang:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!(settings.mode, Some("golang".to_string())); + + let content = "vim: set filetype=js:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!(settings.mode, Some("js".to_string())); + } + + #[test] + fn test_vim_extra_variables() { + // Test that unknown vim options are stored as extra variables + let content = "vim: set foldmethod=marker conceallevel=2 custom=value:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + + assert!( + settings + .vim_extra_variables + .contains(&("foldmethod".to_string(), Some("marker".to_string()))) + ); + assert!( + settings + .vim_extra_variables + .contains(&("conceallevel".to_string(), Some("2".to_string()))) + ); + assert!( + settings + .vim_extra_variables + .contains(&("custom".to_string(), Some("value".to_string()))) + ); + } + + #[test] + fn test_modeline_position() { + // Test modeline in first lines + let first_lines = ["#!/bin/bash", "# vim: set ft=bash ts=4:"]; + let settings = parse_modeline(&first_lines, &[]).unwrap(); + assert_eq!(settings.mode, Some("bash".to_string())); + + // Test modeline in last lines + let last_lines = ["", "/* vim: set ft=c: */"]; + let settings = parse_modeline(&[], &last_lines).unwrap(); + assert_eq!(settings.mode, Some("c".to_string())); + + // Test no modeline found + let content = ["regular content", "no modeline here"]; + assert!(parse_modeline(&content, &content).is_none()); + } + + #[test] + fn test_vim_modeline_version_checks() { + // Note: Current implementation doesn't support version checks yet + // These are tests for future implementation based on vim spec + + // Test version-specific modelines (currently ignored in our implementation) + let content = "/* vim700: set foldmethod=marker */"; + // Should be ignored for now since we don't support version checks + assert!(parse_modeline(&[content], &[]).is_none()); + + let content = "/* vim>702: set cole=2: */"; + // Should be ignored for now since we don't support version checks + assert!(parse_modeline(&[content], &[]).is_none()); + } + + #[test] + fn test_vim_modeline_colon_escaping() { + // Test colon escaping as mentioned in vim spec + + // According to vim spec: "if you want to include a ':' in a set command precede it with a '\'" + let content = r#"/* vim: set fdm=expr fde=getline(v\:lnum)=~'{'?'>1'\:'1': */"#; + + let result = parse_modeline(&[content], &[]).unwrap(); + + // The modeline should parse fdm=expr and fde=getline(v:lnum)=~'{'?'>1':'1' + // as extra variables since they're not recognized settings + assert_eq!(result.vim_extra_variables.len(), 2); + assert_eq!( + result.vim_extra_variables[0], + ("fdm".to_string(), Some("expr".to_string())) + ); + assert_eq!( + result.vim_extra_variables[1], + ( + "fde".to_string(), + Some("getline(v:lnum)=~'{'?'>1':'1'".to_string()) + ) + ); + } + + #[test] + fn test_vim_modeline_whitespace_requirements() { + // Test whitespace requirements from vim spec + + // Valid: whitespace before vi/vim + let content = " vim: set ft=rust:"; + assert!(parse_modeline(&[content], &[]).is_some()); + + // Valid: tab before vi/vim + let content = "\tvim: set ft=rust:"; + assert!(parse_modeline(&[content], &[]).is_some()); + + // Valid: vi/vim at start of line (compatibility) + let content = "vim: set ft=rust:"; + assert!(parse_modeline(&[content], &[]).is_some()); + } + + #[test] + fn test_vim_modeline_comprehensive_examples() { + // Real-world examples from vim documentation and common usage + + // Python example + let content = "# vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!(settings.hard_tabs, Some(false)); + assert_eq!(settings.tab_size, Some(NonZeroU32::new(4).unwrap())); + + // C example with multiple options + let content = "/* vim: set ts=8 sw=8 noet ai cindent: */"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!(settings.tab_size, Some(NonZeroU32::new(8).unwrap())); + assert_eq!(settings.hard_tabs, Some(true)); + assert!( + settings + .vim_extra_variables + .contains(&("cindent".to_string(), None)) + ); + + // Shell script example + let content = "# vi: set ft=sh ts=2 sw=2 et:"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!(settings.mode, Some("sh".to_string())); + assert_eq!(settings.tab_size, Some(NonZeroU32::new(2).unwrap())); + assert_eq!(settings.hard_tabs, Some(false)); + + // First form colon-separated + let content = "vim:ft=xml:ts=2:sw=2:et"; + let settings = parse_modeline(&[content], &[]).unwrap(); + assert_eq!(settings.mode, Some("xml".to_string())); + assert_eq!(settings.tab_size, Some(NonZeroU32::new(2).unwrap())); + assert_eq!(settings.hard_tabs, Some(false)); + } + + #[test] + fn test_combined_emacs_vim_detection() { + // Test that both emacs and vim modelines can be detected in the same file + + let first_lines = [ + "#!/usr/bin/env python3", + "# -*- require-final-newline: t; -*-", + "# vim: set ft=python ts=4 sw=4 et:", + ]; + + // Should find the emacs modeline first (with coding) + let settings = parse_modeline(&first_lines, &[]).unwrap(); + assert_eq!(settings.ensure_final_newline, Some(true)); + assert_eq!(settings.tab_size, None); + + // Test vim-only content + let vim_only = ["# vim: set ft=python ts=4 sw=4 et:"]; + let settings = parse_modeline(&vim_only, &[]).unwrap(); + assert_eq!(settings.mode, Some("python".to_string())); + assert_eq!(settings.tab_size, Some(NonZeroU32::new(4).unwrap())); + assert_eq!(settings.hard_tabs, Some(false)); + } +} diff --git a/crates/language/src/task_context.rs b/crates/language/src/task_context.rs index b8cc6d13fff14576ca938e36d8982973f6307912..dc59d21bd73a2d4a8e1d4a4c765195afffd2ce67 100644 --- a/crates/language/src/task_context.rs +++ b/crates/language/src/task_context.rs @@ -1,11 +1,11 @@ use std::{ops::Range, path::PathBuf, sync::Arc}; -use crate::{File, LanguageToolchainStore, Location, Runnable}; +use crate::{Buffer, LanguageToolchainStore, Location, Runnable}; use anyhow::Result; use collections::HashMap; use fs::Fs; -use gpui::{App, Task}; +use gpui::{App, Entity, Task}; use lsp::LanguageServerName; use task::{TaskTemplates, TaskVariables}; use text::BufferId; @@ -37,7 +37,7 @@ pub trait ContextProvider: Send + Sync { } /// Provides all tasks, associated with the current language. - fn associated_tasks(&self, _: Option>, _: &App) -> Task> { + fn associated_tasks(&self, _: Option>, _: &App) -> Task> { Task::ready(None) } diff --git a/crates/languages/src/bash/config.toml b/crates/languages/src/bash/config.toml index 8ff4802aee5124201d013e0b2f0b01c7046e55a0..06574629f186800f4d95244d7c4129cbc6505d22 100644 --- a/crates/languages/src/bash/config.toml +++ b/crates/languages/src/bash/config.toml @@ -2,6 +2,7 @@ name = "Shell Script" code_fence_block_name = "bash" grammar = "bash" path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "bats", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile", ".env", "PKGBUILD", "APKBUILD"] +modeline_aliases = ["sh", "shell", "zsh", "fish"] line_comments = ["# "] first_line_pattern = '^#!.*\b(?:ash|bash|bats|dash|sh|zsh)\b' autoclose_before = "}])" diff --git a/crates/languages/src/cpp/config.toml b/crates/languages/src/cpp/config.toml index b8e1136725b8633f07e5f864a867dc4d7dc5d77e..dfce8ae7b2bfcfc7a7004822e9c6dca18e6cbe26 100644 --- a/crates/languages/src/cpp/config.toml +++ b/crates/languages/src/cpp/config.toml @@ -1,6 +1,7 @@ name = "C++" grammar = "cpp" path_suffixes = ["cc", "ccm", "hh", "cpp", "cppm", "h", "hpp", "cxx", "cxxm", "hxx", "c++", "c++m", "h++", "ipp", "inl", "ino", "ixx", "cu", "cuh", "C", "H"] +modeline_aliases = ["c++", "cpp", "cxx"] line_comments = ["// ", "/// ", "//! "] first_line_pattern = '^//.*-\*-\s*C\+\+\s*-\*-' decrease_indent_patterns = [ diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 8d945ba3b9e1b501d52675ada80bea41d394d4ed..461327d62731ec10ee862cdd78f5e484b917e495 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -2,12 +2,12 @@ use anyhow::{Context as _, Result}; use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; -use gpui::{App, AsyncApp, Task}; +use gpui::{App, AsyncApp, Entity, Task}; use http_client::github::latest_github_release; pub use language::*; use language::{ LanguageName, LanguageToolchainStore, LspAdapterDelegate, LspInstaller, - language_settings::language_settings, + language_settings::LanguageSettings, }; use lsp::{LanguageServerBinary, LanguageServerName}; @@ -211,7 +211,7 @@ impl LspAdapter for GoLspAdapter { cx: &mut AsyncApp, ) -> Result> { let semantic_tokens_enabled = cx.update(|cx| { - language_settings(Some(LanguageName::new("Go")), None, cx) + LanguageSettings::resolve(None, Some(&LanguageName::new("Go")), cx) .semantic_tokens .enabled() }); @@ -593,7 +593,7 @@ impl ContextProvider for GoContextProvider { ))) } - fn associated_tasks(&self, _: Option>, _: &App) -> Task> { + fn associated_tasks(&self, _: Option>, _: &App) -> Task> { let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." { None } else { diff --git a/crates/languages/src/go/config.toml b/crates/languages/src/go/config.toml index c8589b14d68aa66cd189940c65618b7736b4bfd7..36f885d75fe623eb16b306f0481ac7677ab0d35b 100644 --- a/crates/languages/src/go/config.toml +++ b/crates/languages/src/go/config.toml @@ -1,6 +1,7 @@ name = "Go" grammar = "go" path_suffixes = ["go"] +modeline_aliases = ["golang"] line_comments = ["// "] first_line_pattern = '^//.*\bgo run\b' autoclose_before = ";:.,=}])>" diff --git a/crates/languages/src/javascript/config.toml b/crates/languages/src/javascript/config.toml index 265f362ce4b655371471649c03c5a4a201da320c..2850fd6bc47fe7d23fdfbf9588b2331fdef6e0fa 100644 --- a/crates/languages/src/javascript/config.toml +++ b/crates/languages/src/javascript/config.toml @@ -1,6 +1,7 @@ name = "JavaScript" grammar = "tsx" path_suffixes = ["js", "jsx", "mjs", "cjs"] +modeline_aliases = ["js", "js2"] # [/ ] is so we match "env node" or "/node" but not "ts-node" first_line_pattern = '^#!.*\b(?:[/ ]node|deno run.*--ext[= ]js)\b' line_comments = ["// "] diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 3d8ba972eb17b0fe7f9d5070b73a4fb9e94adef3..de30d958d006016a118f2db077e38c1212f4f683 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -4,10 +4,10 @@ use async_tar::Archive; use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; -use gpui::{App, AsyncApp, Task}; +use gpui::{App, AsyncApp, Entity, Task}; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; use language::{ - ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter, + Buffer, ContextProvider, LanguageName, LanguageRegistry, LocalFile as _, LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain, }; use lsp::{LanguageServerBinary, LanguageServerName, Uri}; @@ -44,10 +44,11 @@ pub(crate) struct JsonTaskProvider; impl ContextProvider for JsonTaskProvider { fn associated_tasks( &self, - file: Option>, + buffer: Option>, cx: &App, ) -> gpui::Task> { - let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else { + let file = buffer.as_ref().and_then(|buf| buf.read(cx).file()); + let Some(file) = project::File::from_dyn(file).cloned() else { return Task::ready(None); }; let is_package_json = file.path.ends_with(RelPath::unix("package.json").unwrap()); diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 53aa24e9e87f1590d98fc5b038a7add582f1e944..071a288d79c3afd467944106fd3535be94db0f0f 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -51,6 +51,7 @@ pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock> = matcher: LanguageMatcher { path_suffixes: vec!["COMMIT_EDITMSG".to_owned()], first_line_pattern: None, + ..LanguageMatcher::default() }, line_comments: vec![Arc::from("#")], ..LanguageConfig::default() diff --git a/crates/languages/src/markdown/config.toml b/crates/languages/src/markdown/config.toml index 5e7acd230b6f191aebff609bbc1087fbff8d3909..27dd1821e414fb8e068c3c1975ec6189d80c0350 100644 --- a/crates/languages/src/markdown/config.toml +++ b/crates/languages/src/markdown/config.toml @@ -1,6 +1,7 @@ name = "Markdown" grammar = "markdown" path_suffixes = ["md", "mdx", "mdwn", "mdc", "markdown", "MD"] +modeline_aliases = ["md"] completion_query_characters = ["-"] block_comment = { start = "", tab_size = 0 } autoclose_before = ";:.,=}])>" diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index ef750f5ce6efde71450d7c24c585d4256bd9d03b..ae506f22b12158ca07415f8ebfe2dbbef5b9ee33 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -5,10 +5,12 @@ use collections::HashMap; use futures::future::BoxFuture; use futures::lock::OwnedMutexGuard; use futures::{AsyncBufReadExt, StreamExt as _}; -use gpui::{App, AsyncApp, SharedString, Task}; +use gpui::{App, AsyncApp, Entity, SharedString, Task}; use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release}; -use language::language_settings::language_settings; -use language::{ContextLocation, DynLspInstaller, LanguageToolchainStore, LspInstaller, Symbol}; +use language::language_settings::LanguageSettings; +use language::{ + Buffer, ContextLocation, DynLspInstaller, LanguageToolchainStore, LspInstaller, Symbol, +}; use language::{ContextProvider, LspAdapter, LspAdapterDelegate}; use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata}; @@ -831,11 +833,10 @@ impl ContextProvider for PythonContextProvider { toolchains: Arc, cx: &mut gpui::App, ) -> Task> { - let test_target = - match selected_test_runner(location.file_location.buffer.read(cx).file(), cx) { - TestRunner::UNITTEST => self.build_unittest_target(variables), - TestRunner::PYTEST => self.build_pytest_target(variables), - }; + let test_target = match selected_test_runner(Some(&location.file_location.buffer), cx) { + TestRunner::UNITTEST => self.build_unittest_target(variables), + TestRunner::PYTEST => self.build_pytest_target(variables), + }; let module_target = self.build_module_target(variables); let location_file = location.file_location.buffer.read(cx).file().cloned(); @@ -873,10 +874,10 @@ impl ContextProvider for PythonContextProvider { fn associated_tasks( &self, - file: Option>, + buffer: Option>, cx: &App, ) -> Task> { - let test_runner = selected_test_runner(file.as_ref(), cx); + let test_runner = selected_test_runner(buffer.as_ref(), cx); let mut tasks = vec![ // Execute a selection @@ -983,9 +984,11 @@ impl ContextProvider for PythonContextProvider { } } -fn selected_test_runner(location: Option<&Arc>, cx: &App) -> TestRunner { +fn selected_test_runner(location: Option<&Entity>, cx: &App) -> TestRunner { const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER"; - language_settings(Some(LanguageName::new_static("Python")), location, cx) + let language = LanguageName::new_static("Python"); + let settings = LanguageSettings::resolve(location.map(|b| b.read(cx)), Some(&language), cx); + settings .tasks .variables .get(TEST_RUNNER_VARIABLE) diff --git a/crates/languages/src/python/config.toml b/crates/languages/src/python/config.toml index d96a5ea5fefd0814c4b0787251e5cf6e4c166d5e..fa409c5dd6519121e7130e4b33a6c3277ae1654b 100644 --- a/crates/languages/src/python/config.toml +++ b/crates/languages/src/python/config.toml @@ -2,6 +2,7 @@ name = "Python" grammar = "python" path_suffixes = ["py", "pyi", "mpy"] first_line_pattern = '^#!.*((\bpython[0-9.]*\b)|(\buv run\b))' +modeline_aliases = ["py"] line_comments = ["# "] autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/languages/src/rust.rs b/crates/languages/src/rust.rs index e463a6c62dd6a0625c8ee7c6d314b296b881157e..aa2c94cd95da503e1230b677032219e44cb54014 100644 --- a/crates/languages/src/rust.rs +++ b/crates/languages/src/rust.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; use futures::lock::OwnedMutexGuard; -use gpui::{App, AppContext, AsyncApp, SharedString, Task}; +use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task}; use http_client::github::AssetKind; use http_client::github::{GitHubLspBinaryVersion, latest_github_release}; use http_client::github_download::{GithubBinaryMetadata, download_server_binary}; @@ -32,7 +32,7 @@ use util::rel_path::RelPath; use util::{ResultExt, maybe}; use crate::LanguageDir; -use crate::language_settings::language_settings; +use crate::language_settings::LanguageSettings; pub(crate) fn semantic_token_rules() -> SemanticTokenRules { let content = LanguageDir::get("rust/semantic_token_rules.json") @@ -898,23 +898,16 @@ impl ContextProvider for RustContextProvider { fn associated_tasks( &self, - file: Option>, + buffer: Option>, cx: &App, ) -> Task> { const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN"; const CUSTOM_TARGET_DIR: &str = "RUST_TARGET_DIR"; - let language_sets = language_settings(Some("Rust".into()), file.as_ref(), cx); - let package_to_run = language_sets - .tasks - .variables - .get(DEFAULT_RUN_NAME_STR) - .cloned(); - let custom_target_dir = language_sets - .tasks - .variables - .get(CUSTOM_TARGET_DIR) - .cloned(); + let language = LanguageName::new_static("Rust"); + let settings = LanguageSettings::resolve(buffer.map(|b| b.read(cx)), Some(&language), cx); + let package_to_run = settings.tasks.variables.get(DEFAULT_RUN_NAME_STR).cloned(); + let custom_target_dir = settings.tasks.variables.get(CUSTOM_TARGET_DIR).cloned(); let run_task_args = if let Some(package_to_run) = package_to_run { vec!["run".into(), "-p".into(), package_to_run] } else { diff --git a/crates/languages/src/rust/config.toml b/crates/languages/src/rust/config.toml index 826a219e9868a3f76a063efe8c91cec0be14c2da..203a44853f8bd20f952d3db8f0c64dc4babe1017 100644 --- a/crates/languages/src/rust/config.toml +++ b/crates/languages/src/rust/config.toml @@ -1,6 +1,7 @@ name = "Rust" grammar = "rust" path_suffixes = ["rs"] +modeline_aliases = ["rs", "rustic"] line_comments = ["// ", "/// ", "//! "] autoclose_before = ";:.,=}])>" brackets = [ diff --git a/crates/languages/src/tsx/config.toml b/crates/languages/src/tsx/config.toml index d0a4eb6532db621d741df2fbc99125e1c037ccdf..42438fdf890a98f319244332f384f574e02c2904 100644 --- a/crates/languages/src/tsx/config.toml +++ b/crates/languages/src/tsx/config.toml @@ -1,6 +1,7 @@ name = "TSX" grammar = "tsx" path_suffixes = ["tsx"] +modeline_aliases = ["typescript-txs"] line_comments = ["// "] block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 } diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index d15d01808137dd171cc7ee0ab440671bf58cac52..714191ace093aa4c592316692dae3db0cdc24223 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -3,11 +3,11 @@ use async_trait::async_trait; use chrono::{DateTime, Local}; use collections::HashMap; use futures::future::join_all; -use gpui::{App, AppContext, AsyncApp, Task}; +use gpui::{App, AppContext, AsyncApp, Entity, Task}; use itertools::Itertools as _; use language::{ - ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, LspAdapter, - LspAdapterDelegate, LspInstaller, Toolchain, + Buffer, ContextLocation, ContextProvider, File, LanguageName, LanguageToolchainStore, + LspAdapter, LspAdapterDelegate, LspInstaller, Toolchain, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName, Uri}; use node_runtime::{NodeRuntime, VersionStrategy}; @@ -425,10 +425,11 @@ async fn detect_package_manager( impl ContextProvider for TypeScriptContextProvider { fn associated_tasks( &self, - file: Option>, + buffer: Option>, cx: &App, ) -> Task> { - let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else { + let file = buffer.and_then(|buffer| buffer.read(cx).file()); + let Some(file) = project::File::from_dyn(file).cloned() else { return Task::ready(None); }; let Some(worktree_root) = file.worktree.read(cx).root_dir() else { diff --git a/crates/languages/src/typescript/config.toml b/crates/languages/src/typescript/config.toml index 67656e6a538da6c8860e9ab1b08fd6e6ee9cabbd..c0e8a8899a99b0b65e2d073547f3eaf0fe714da2 100644 --- a/crates/languages/src/typescript/config.toml +++ b/crates/languages/src/typescript/config.toml @@ -1,6 +1,7 @@ name = "TypeScript" grammar = "typescript" path_suffixes = ["ts", "cts", "mts"] +modeline_aliases = ["ts"] first_line_pattern = '^#!.*\b(?:deno run|ts-node|bun|tsx|[/ ]node)\b' line_comments = ["// "] block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } diff --git a/crates/languages/src/yaml/config.toml b/crates/languages/src/yaml/config.toml index 9a07a560b06766ac00dd73b6210023c4cddd491d..95fe81d04dbbb88e1c7deed7a84895cddb7dea1d 100644 --- a/crates/languages/src/yaml/config.toml +++ b/crates/languages/src/yaml/config.toml @@ -1,6 +1,7 @@ name = "YAML" grammar = "yaml" path_suffixes = ["yml", "yaml", "pixi.lock", "clang-format", "clangd", "bst"] +modeline_aliases = ["yml"] line_comments = ["# "] autoclose_before = ",]}" brackets = [ diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 3b1177874cac0f71d4652aa0948005397e362b58..a1739629c6af498eccb6ea90b8c866e9cc7946b4 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -23,13 +23,14 @@ use language::{ IndentGuideSettings, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _, ToPoint as _, TransactionId, TreeSitterOptions, Unclipped, - language_settings::{LanguageSettings, language_settings}, + language_settings::{AllLanguageSettings, LanguageSettings}, }; #[cfg(any(test, feature = "test-support"))] use gpui::AppContext as _; use rope::DimensionPair; +use settings::Settings; use smallvec::SmallVec; use smol::future::yield_now; use std::{ @@ -2576,10 +2577,7 @@ impl MultiBuffer { .map(|excerpt| excerpt.buffer.remote_id()); buffer_id .and_then(|buffer_id| self.buffer(buffer_id)) - .map(|buffer| { - let buffer = buffer.read(cx); - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - }) + .map(|buffer| LanguageSettings::for_buffer(&buffer.read(cx), cx)) .unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::default(), cx)) } @@ -2588,14 +2586,11 @@ impl MultiBuffer { point: T, cx: &'a App, ) -> Cow<'a, LanguageSettings> { - let mut language = None; - let mut file = None; if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { - let buffer = buffer.read(cx); - language = buffer.language_at(offset); - file = buffer.file(); + LanguageSettings::for_buffer_at(buffer.read(cx), offset, cx) + } else { + Cow::Borrowed(&AllLanguageSettings::get_global(cx).defaults) } - language_settings(language.map(|l| l.name()), file, cx) } pub fn for_each_buffer(&self, f: &mut dyn FnMut(&Entity)) { @@ -6603,8 +6598,7 @@ impl MultiBufferSnapshot { let end_row = MultiBufferRow(range.end.row); let mut row_indents = self.line_indents(start_row, |buffer| { - let settings = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx); + let settings = LanguageSettings::for_buffer_snapshot(buffer, None, cx); settings.indent_guides.enabled || ignore_disabled_for_language }); @@ -6628,7 +6622,7 @@ impl MultiBufferSnapshot { .get_or_insert_with(|| { ( buffer.remote_id(), - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx), + LanguageSettings::for_buffer_snapshot(buffer, None, cx), ) }) .1; @@ -6724,13 +6718,7 @@ impl MultiBufferSnapshot { self.excerpts .first() .map(|excerpt| &excerpt.buffer) - .map(|buffer| { - language_settings( - buffer.language().map(|language| language.name()), - buffer.file(), - cx, - ) - }) + .map(|buffer| LanguageSettings::for_buffer_snapshot(buffer, None, cx)) .unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::ZERO, cx)) } @@ -6739,13 +6727,11 @@ impl MultiBufferSnapshot { point: T, cx: &'a App, ) -> Cow<'a, LanguageSettings> { - let mut language = None; - let mut file = None; if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { - language = buffer.language_at(offset); - file = buffer.file(); + buffer.settings_at(offset, cx) + } else { + Cow::Borrowed(&AllLanguageSettings::get_global(cx).defaults) } - language_settings(language.map(|l| l.name()), file, cx) } pub fn language_scope_at(&self, point: T) -> Option { diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 6b69e8d0cfd8e4acaa6eaff017c3b1d3f23833b9..83d9d51a0e85e50a1a1dae6dad4e797d6763e58d 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -23,7 +23,7 @@ use gpui::{ uniform_list, }; use itertools::Itertools; -use language::language_settings::language_settings; +use language::language_settings::LanguageSettings; use language::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious}; use std::{ @@ -855,12 +855,8 @@ impl OutlinePanel { .read(cx) .buffer_for_id(*buffer_id, cx)?; let buffer = buffer.read(cx); - let doc_symbols = language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ) - .document_symbols; + let doc_symbols = + LanguageSettings::for_buffer(buffer, cx).document_symbols; Some((*buffer_id, doc_symbols)) }) .collect(); diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index 90f512a5931fa89ac9b8a2216091f3633f872b6b..b0fd57f6980ca0f0f4d6d95ecd0e994ab80b2016 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -2,8 +2,8 @@ use anyhow::Context as _; use collections::{HashMap, HashSet}; use fs::Fs; use gpui::{AsyncApp, Entity}; -use language::language_settings::PrettierSettings; -use language::{Buffer, Diff, Language, language_settings::language_settings}; +use language::language_settings::{LanguageSettings, PrettierSettings}; +use language::{Buffer, Diff, Language}; use lsp::{LanguageServer, LanguageServerId}; use node_runtime::NodeRuntime; use paths::default_prettier_dir; @@ -356,7 +356,7 @@ impl Prettier { let params = buffer .update(cx, |buffer, cx| { let buffer_language = buffer.language().map(|language| language.as_ref()); - let language_settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); + let language_settings = LanguageSettings::for_buffer(&buffer, cx); let prettier_settings = &language_settings.prettier; anyhow::ensure!( prettier_settings.allowed, @@ -505,11 +505,7 @@ impl Prettier { let buffer_language = buffer.language().map(|language| language.as_ref()); - let language_settings = language_settings( - buffer_language.map(|l| l.name()), - buffer.file(), - cx, - ); + let language_settings = LanguageSettings::for_buffer(buffer, cx); let prettier_settings = &language_settings.prettier; let parser = prettier_parser_name( buffer_path.as_deref(), diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c5cd568fb88c0c1de66adb99bb47f96508a6df04..59baaa156e64c744d8906d294f7f3978280a1839 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -18,7 +18,7 @@ use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::FluentBuilder}; use language::{ Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CharScopeContext, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, - language_settings::{InlayHintKind, LanguageSettings, language_settings}, + language_settings::{InlayHintKind, LanguageSettings}, point_from_lsp, point_to_lsp, proto::{ deserialize_anchor, deserialize_anchor_range, deserialize_version, serialize_anchor, @@ -2936,9 +2936,7 @@ impl LspCommand for OnTypeFormatting { .await?; let options = buffer.update(&mut cx, |buffer, cx| { - lsp_formatting_options( - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).as_ref(), - ) + lsp_formatting_options(LanguageSettings::for_buffer(buffer, cx).as_ref()) }); Ok(Self { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 9b0a14cbeaa83a1a7591f50fb913096a7d2af248..b08aa8df27692d6abf7fce35e71a2381969e1a59 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -73,13 +73,12 @@ use language::{ Bias, BinaryStatus, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, LspInstaller, - ManifestDelegate, ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, - Toolchain, Transaction, Unclipped, + ManifestDelegate, ManifestName, ModelineSettings, Patch, PointUtf16, TextBufferSnapshot, + ToOffset, ToPointUtf16, Toolchain, Transaction, Unclipped, language_settings::{ AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, all_language_settings, - language_settings, }, - point_to_lsp, + modeline, point_to_lsp, proto::{ deserialize_anchor, deserialize_anchor_range, deserialize_version, serialize_anchor, serialize_anchor_range, serialize_version, @@ -1601,9 +1600,7 @@ impl LocalLspStore { .language_servers_for_buffer(buffer, cx) .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) .collect::>(); - let settings = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - .into_owned(); + let settings = LanguageSettings::for_buffer(buffer, cx).into_owned(); let request_timeout = ProjectSettings::get_global(cx) .global_lsp_settings .get_request_timeout(); @@ -4464,6 +4461,7 @@ impl LspStore { }) .detach(); + self.parse_modeline(buffer, cx); self.detect_language_for_buffer(buffer, cx); if let Some(local) = self.as_local_mut() { local.initialize_buffer(buffer, cx); @@ -4513,6 +4511,16 @@ impl LspStore { }) } + fn on_buffer_reloaded(&mut self, buffer: Entity, cx: &mut Context) { + if self.parse_modeline(&buffer, cx) { + self.detect_language_for_buffer(&buffer, cx); + } + + let buffer_id = buffer.read(cx).remote_id(); + let task = self.pull_diagnostics_for_buffer(buffer, cx); + self.buffer_reload_tasks.insert(buffer_id, task); + } + pub(crate) fn register_buffer_with_language_servers( &mut self, buffer: &Entity, @@ -4736,6 +4744,56 @@ impl LspStore { }) } + fn parse_modeline(&mut self, buffer_handle: &Entity, cx: &mut Context) -> bool { + let buffer = buffer_handle.read(cx); + let content = buffer.as_rope(); + + let modeline_settings = { + let settings_store = cx.global::(); + let modeline_lines = settings_store + .raw_user_settings() + .and_then(|s| s.content.modeline_lines) + .or(settings_store.raw_default_settings().modeline_lines) + .unwrap_or(5); + + const MAX_MODELINE_BYTES: usize = 1024; + + let first_bytes = + content.clip_offset(content.len().min(MAX_MODELINE_BYTES), Bias::Left); + let mut first_lines = Vec::new(); + let mut lines = content.chunks_in_range(0..first_bytes).lines(); + for _ in 0..modeline_lines { + if let Some(line) = lines.next() { + first_lines.push(line.to_string()); + } else { + break; + } + } + let first_lines_ref: Vec<_> = first_lines.iter().map(|line| line.as_str()).collect(); + + let last_start = + content.clip_offset(content.len().saturating_sub(MAX_MODELINE_BYTES), Bias::Left); + let mut last_lines = Vec::new(); + let mut lines = content + .reversed_chunks_in_range(last_start..content.len()) + .lines(); + for _ in 0..modeline_lines { + if let Some(line) = lines.next() { + last_lines.push(line.to_string()); + } else { + break; + } + } + let last_lines_ref: Vec<_> = + last_lines.iter().rev().map(|line| line.as_str()).collect(); + modeline::parse_modeline(&first_lines_ref, &last_lines_ref) + }; + + log::debug!("Parsed modeline settings: {:?}", modeline_settings); + + buffer_handle.update(cx, |buffer, _cx| buffer.set_modeline(modeline_settings)) + } + fn detect_language_for_buffer( &mut self, buffer_handle: &Entity, @@ -4744,9 +4802,19 @@ impl LspStore { // If the buffer has a language, set it and start the language server if we haven't already. let buffer = buffer_handle.read(cx); let file = buffer.file()?; - let content = buffer.as_rope(); - let available_language = self.languages.language_for_file(file, Some(content), cx); + let modeline_settings = buffer.modeline().map(Arc::as_ref); + + let available_language = if let Some(ModelineSettings { + mode: Some(mode_name), + .. + }) = modeline_settings + { + self.languages + .available_language_for_modeline_name(mode_name) + } else { + self.languages.language_for_file(file, Some(content), cx) + }; if let Some(available_language) = &available_language { if let Some(Ok(Ok(new_language))) = self .languages @@ -4791,8 +4859,12 @@ impl LspStore { } }); - let settings = - language_settings(Some(new_language.name()), buffer_file.as_ref(), cx).into_owned(); + let settings = LanguageSettings::resolve( + Some(&buffer_entity.read(cx)), + Some(&new_language.name()), + cx, + ) + .into_owned(); let buffer_file = File::from_dyn(buffer_file.as_ref()); let worktree_id = if let Some(file) = buffer_file { @@ -5100,10 +5172,9 @@ impl LspStore { let mut language_formatters_to_check = Vec::new(); for buffer in self.buffer_store.read(cx).buffers() { let buffer = buffer.read(cx); - let buffer_file = File::from_dyn(buffer.file()); - let buffer_language = buffer.language(); - let settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); - if buffer_language.is_some() { + let settings = LanguageSettings::for_buffer(buffer, cx); + if buffer.language().is_some() { + let buffer_file = File::from_dyn(buffer.file()); language_formatters_to_check.push(( buffer_file.map(|f| f.worktree_id(cx)), settings.into_owned(), @@ -5553,9 +5624,9 @@ impl LspStore { }) .filter(|_| { maybe!({ - let language = buffer.read(cx).language_at(position)?; + buffer.read(cx).language_at(position)?; Some( - language_settings(Some(language.name()), buffer.read(cx).file(), cx) + LanguageSettings::for_buffer_at(&buffer.read(cx), position, cx) .linked_edits, ) }) == Some(true) @@ -5659,12 +5730,7 @@ impl LspStore { ) -> Task>> { let options = buffer.update(cx, |buffer, cx| { lsp_command::lsp_formatting_options( - language_settings( - buffer.language_at(position).map(|l| l.name()), - buffer.file(), - cx, - ) - .as_ref(), + LanguageSettings::for_buffer_at(buffer, position, cx).as_ref(), ) }); @@ -6206,13 +6272,9 @@ impl LspStore { let offset = position.to_offset(&snapshot); let scope = snapshot.language_scope_at(offset); let language = snapshot.language().cloned(); - let completion_settings = language_settings( - language.as_ref().map(|language| language.name()), - buffer.read(cx).file(), - cx, - ) - .completions - .clone(); + let completion_settings = LanguageSettings::for_buffer(&buffer.read(cx), cx) + .completions + .clone(); if !completion_settings.lsp { return Task::ready(Ok(Vec::new())); } @@ -7966,12 +8028,6 @@ impl LspStore { None } - fn on_buffer_reloaded(&mut self, buffer: Entity, cx: &mut Context) { - let buffer_id = buffer.read(cx).remote_id(); - let task = self.pull_diagnostics_for_buffer(buffer, cx); - self.buffer_reload_tasks.insert(buffer_id, task); - } - async fn refresh_workspace_configurations(lsp_store: &WeakEntity, cx: &mut AsyncApp) { maybe!(async move { let mut refreshed_servers = HashSet::default(); diff --git a/crates/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 2cd62a9fa83f78b4f2f64cd8cd6503fcf0fb5f29..46999b2b7024c6035732b64de30a3e64cd65460c 100644 --- a/crates/project/src/task_inventory.rs +++ b/crates/project/src/task_inventory.rs @@ -15,7 +15,7 @@ use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity use itertools::Itertools; use language::{ Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location, - language_settings::language_settings, + language_settings::LanguageSettings, }; use lsp::{LanguageServerId, LanguageServerName}; use paths::{debug_task_file_name, task_file_name}; @@ -312,17 +312,15 @@ impl Inventory { let last_scheduled_scenarios = self.last_scheduled_scenarios.iter().cloned().collect(); let adapter = task_contexts.location().and_then(|location| { - let (file, language) = { - let buffer = location.buffer.read(cx); - (buffer.file(), buffer.language()) - }; - let language_name = language.as_ref().map(|l| l.name()); - let adapter = language_settings(language_name, file, cx) + let buffer = location.buffer.read(cx); + let adapter = LanguageSettings::for_buffer(&buffer, cx) .debuggers .first() .map(SharedString::from) .or_else(|| { - language.and_then(|l| l.config().debuggers.first().map(SharedString::from)) + buffer + .language() + .and_then(|l| l.config().debuggers.first().map(SharedString::from)) }); adapter.map(|adapter| (adapter, DapRegistry::global(cx).locators())) }); @@ -360,19 +358,18 @@ impl Inventory { label: &str, cx: &App, ) -> Task> { - let (buffer_worktree_id, file, language) = buffer + let (buffer_worktree_id, language) = buffer + .as_ref() .map(|buffer| { let buffer = buffer.read(cx); - let file = buffer.file().cloned(); ( - file.as_ref().map(|file| file.worktree_id(cx)), - file, + buffer.file().as_ref().map(|file| file.worktree_id(cx)), buffer.language().cloned(), ) }) - .unwrap_or((None, None, None)); + .unwrap_or((None, None)); - let tasks = self.list_tasks(file, language, worktree_id.or(buffer_worktree_id), cx); + let tasks = self.list_tasks(buffer, language, worktree_id.or(buffer_worktree_id), cx); let label = label.to_owned(); cx.background_spawn(async move { tasks @@ -388,7 +385,7 @@ impl Inventory { /// and global tasks last. No specific order inside source kinds groups. pub fn list_tasks( &self, - file: Option>, + buffer: Option>, language: Option>, worktree: Option, cx: &App, @@ -404,14 +401,18 @@ impl Inventory { }); let language_tasks = language .filter(|language| { - language_settings(Some(language.name()), file.as_ref(), cx) - .tasks - .enabled + LanguageSettings::resolve( + buffer.as_ref().map(|b| b.read(cx)), + Some(&language.name()), + cx, + ) + .tasks + .enabled }) .and_then(|language| { language .context_provider() - .map(|provider| provider.associated_tasks(file, cx)) + .map(|provider| provider.associated_tasks(buffer, cx)) }); cx.background_spawn(async move { if let Some(t) = language_tasks { @@ -445,7 +446,7 @@ impl Inventory { let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language { name: language.name().into(), }); - let file = location.and_then(|location| location.buffer.read(cx).file().cloned()); + let buffer = location.map(|location| location.buffer.clone()); let worktrees_with_zed_tasks: HashSet = self .templates_from_settings @@ -507,14 +508,18 @@ impl Inventory { let global_tasks = self.global_templates_from_settings().collect::>(); let associated_tasks = language .filter(|language| { - language_settings(Some(language.name()), file.as_ref(), cx) - .tasks - .enabled + LanguageSettings::resolve( + buffer.as_ref().map(|b| b.read(cx)), + Some(&language.name()), + cx, + ) + .tasks + .enabled }) .and_then(|language| { language .context_provider() - .map(|provider| provider.associated_tasks(file, cx)) + .map(|provider| provider.associated_tasks(buffer, cx)) }); let worktree_tasks = worktree .into_iter() @@ -1036,7 +1041,7 @@ impl ContextProviderWithTasks { } impl ContextProvider for ContextProviderWithTasks { - fn associated_tasks(&self, _: Option>, _: &App) -> Task> { + fn associated_tasks(&self, _: Option>, _: &App) -> Task> { Task::ready(Some(self.templates.clone())) } } diff --git a/crates/project/tests/integration/project_tests.rs b/crates/project/tests/integration/project_tests.rs index b30ada29745e6dd03d3e914223df71ad7edf4de1..5f2c590197b7f5d49d88da4cf97111865580c590 100644 --- a/crates/project/tests/integration/project_tests.rs +++ b/crates/project/tests/integration/project_tests.rs @@ -36,7 +36,7 @@ use git::{ use git2::RepositoryInitOptions; use gpui::{ App, AppContext, BackgroundExecutor, BorrowAppContext, Entity, FutureExt, SharedString, Task, - UpdateGlobal, + TestAppContext, UpdateGlobal, }; use itertools::Itertools; use language::{ @@ -44,7 +44,7 @@ use language::{ DiagnosticSourceKind, DiskState, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, ManifestQuery, OffsetRangeExt, Point, ToPoint, Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata, - language_settings::{LanguageSettingsContent, language_settings}, + language_settings::{LanguageSettings, LanguageSettingsContent}, markdown_lang, rust_lang, tree_sitter_typescript, }; use lsp::{ @@ -296,50 +296,43 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) { cx.executor().run_until_parked(); - cx.update(|cx| { - let tree = worktree.read(cx); - let settings_for = |path: &str| { - let file_entry = tree.entry_for_path(rel_path(path)).unwrap().clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - language_settings(Some(file_language.name()), Some(&file), cx).into_owned() - }; + let settings_for = async |path: &str, cx: &mut TestAppContext| -> LanguageSettings { + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path(path)), cx) + }) + .await + .unwrap(); + cx.update(|cx| LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned()) + }; - let settings_a = settings_for("a.rs"); - let settings_b = settings_for("b/b.rs"); - let settings_c = settings_for("c.js"); - let settings_d = settings_for("d/d.rs"); - let settings_readme = settings_for("README.json"); + let settings_a = settings_for("a.rs", cx).await; + let settings_b = settings_for("b/b.rs", cx).await; + let settings_c = settings_for("c.js", cx).await; + let settings_d = settings_for("d/d.rs", cx).await; + let settings_readme = settings_for("README.json", cx).await; + // .editorconfig overrides .zed/settings + assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3)); + assert_eq!(settings_a.hard_tabs, true); + assert_eq!(settings_a.ensure_final_newline_on_save, true); + assert_eq!(settings_a.remove_trailing_whitespace_on_save, true); + assert_eq!(settings_a.preferred_line_length, 120); - // .editorconfig overrides .zed/settings - assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3)); - assert_eq!(settings_a.hard_tabs, true); - assert_eq!(settings_a.ensure_final_newline_on_save, true); - assert_eq!(settings_a.remove_trailing_whitespace_on_save, true); - assert_eq!(settings_a.preferred_line_length, 120); + // .editorconfig in b/ overrides .editorconfig in root + assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2)); - // .editorconfig in subdirectory overrides .editorconfig in root - assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2)); - assert_eq!(Some(settings_d.tab_size), NonZeroU32::new(1)); + // .editorconfig in subdirectory overrides .editorconfig in root + assert_eq!(Some(settings_d.tab_size), NonZeroU32::new(1)); - // "indent_size" is not set, so "tab_width" is used - assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10)); + // "indent_size" is not set, so "tab_width" is used + assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10)); - // When max_line_length is "off", default to .zed/settings.json - assert_eq!(settings_b.preferred_line_length, 64); - assert_eq!(settings_c.preferred_line_length, 64); + // When max_line_length is "off", default to .zed/settings.json + assert_eq!(settings_b.preferred_line_length, 64); + assert_eq!(settings_c.preferred_line_length, 64); - // README.md should not be affected by .editorconfig's globe "*.rs" - assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8)); - }); + // README.md should not be affected by .editorconfig's globe "*.rs" + assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8)); } #[gpui::test] @@ -373,37 +366,28 @@ async fn test_external_editorconfig_support(cx: &mut gpui::TestAppContext) { let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap()); cx.executor().run_until_parked(); + let settings_for = async |path: &str, cx: &mut TestAppContext| -> LanguageSettings { + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path(path)), cx) + }) + .await + .unwrap(); + cx.update(|cx| LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned()) + }; - cx.update(|cx| { - let tree = worktree.read(cx); - let settings_for = |path: &str| { - let file_entry = tree.entry_for_path(rel_path(path)).unwrap().clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - language_settings(Some(file_language.name()), Some(&file), cx).into_owned() - }; - - let settings_rs = settings_for("main.rs"); - let settings_md = settings_for("README.md"); - let settings_txt = settings_for("other.txt"); + let settings_rs = settings_for("main.rs", cx).await; + let settings_md = settings_for("README.md", cx).await; + let settings_txt = settings_for("other.txt", cx).await; - // main.rs gets indent_size = 2 from parent's external .editorconfig - assert_eq!(Some(settings_rs.tab_size), NonZeroU32::new(2)); + // main.rs gets indent_size = 2 from parent's external .editorconfig + assert_eq!(Some(settings_rs.tab_size), NonZeroU32::new(2)); - // README.md gets indent_size = 3 from internal worktree .editorconfig - assert_eq!(Some(settings_md.tab_size), NonZeroU32::new(3)); + // README.md gets indent_size = 3 from internal worktree .editorconfig + assert_eq!(Some(settings_md.tab_size), NonZeroU32::new(3)); - // other.txt gets indent_size = 4 from grandparent's external .editorconfig - assert_eq!(Some(settings_txt.tab_size), NonZeroU32::new(4)); - }); + // other.txt gets indent_size = 4 from grandparent's external .editorconfig + assert_eq!(Some(settings_txt.tab_size), NonZeroU32::new(4)); } #[gpui::test] @@ -432,24 +416,14 @@ async fn test_internal_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppC cx.executor().run_until_parked(); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path("src/file.rs")), cx) + }) + .await + .unwrap(); cx.update(|cx| { - let tree = worktree.read(cx); - let file_entry = tree - .entry_for_path(rel_path("src/file.rs")) - .unwrap() - .clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); - + let settings = LanguageSettings::for_buffer(buffer.read(cx), cx).into_owned(); assert_eq!(Some(settings.tab_size), NonZeroU32::new(2)); }); } @@ -480,20 +454,15 @@ async fn test_external_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppC cx.executor().run_until_parked(); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx) + }) + .await + .unwrap(); + cx.update(|cx| { - let tree = worktree.read(cx); - let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); + let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx); // file.rs gets indent_size = 2 from worktree's root config, NOT 99 from parent assert_eq!(Some(settings.tab_size), NonZeroU32::new(2)); @@ -528,20 +497,15 @@ async fn test_external_editorconfig_root_in_parent_stops_traversal(cx: &mut gpui cx.executor().run_until_parked(); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx) + }) + .await + .unwrap(); + cx.update(|cx| { - let tree = worktree.read(cx); - let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); + let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx); // file.rs gets indent_size = 4 from parent's root config, NOT 99 from grandparent assert_eq!(Some(settings.tab_size), NonZeroU32::new(4)); @@ -584,30 +548,24 @@ async fn test_external_editorconfig_shared_across_worktrees(cx: &mut gpui::TestA cx.executor().run_until_parked(); - cx.update(|cx| { - let worktrees: Vec<_> = project.read(cx).worktrees(cx).collect(); - assert_eq!(worktrees.len(), 2); + let worktrees: Vec<_> = cx.update(|cx| project.read(cx).worktrees(cx).collect()); + assert_eq!(worktrees.len(), 2); - for worktree in worktrees { - let tree = worktree.read(cx); - let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = - language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); + for worktree in worktrees { + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx) + }) + .await + .unwrap(); + + cx.update(|cx| { + let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx); // Both worktrees should get indent_size = 5 from shared parent .editorconfig assert_eq!(Some(settings.tab_size), NonZeroU32::new(5)); - } - }); + }); + } } #[gpui::test] @@ -637,20 +595,15 @@ async fn test_external_editorconfig_not_loaded_without_internal_config( cx.executor().run_until_parked(); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx) + }) + .await + .unwrap(); + cx.update(|cx| { - let tree = worktree.read(cx); - let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); + let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx); // file.rs should have default tab_size = 4, NOT 99 from parent's external .editorconfig // because without an internal .editorconfig, external configs are not loaded @@ -684,20 +637,15 @@ async fn test_external_editorconfig_modification_triggers_refresh(cx: &mut gpui: cx.executor().run_until_parked(); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx) + }) + .await + .unwrap(); + cx.update(|cx| { - let tree = worktree.read(cx); - let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); + let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx); // Test initial settings: tab_size = 4 from parent's external .editorconfig assert_eq!(Some(settings.tab_size), NonZeroU32::new(4)); @@ -712,20 +660,15 @@ async fn test_external_editorconfig_modification_triggers_refresh(cx: &mut gpui: cx.executor().run_until_parked(); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx) + }) + .await + .unwrap(); + cx.update(|cx| { - let tree = worktree.read(cx); - let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); + let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx); // Test settings updated: tab_size = 8 assert_eq!(Some(settings.tab_size), NonZeroU32::new(8)); @@ -760,21 +703,16 @@ async fn test_adding_worktree_discovers_external_editorconfigs(cx: &mut gpui::Te cx.executor().run_until_parked(); + let buffer = project + .update(cx, |project, cx| { + let id = project.worktrees(cx).next().unwrap().read(cx).id(); + project.open_buffer((id, rel_path("file.rs")), cx) + }) + .await + .unwrap(); + cx.update(|cx| { - let worktree = project.read(cx).worktrees(cx).next().unwrap(); - let tree = worktree.read(cx); - let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone(); - let file = File::for_entry(file_entry, worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); + let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned(); // Test existing worktree has tab_size = 7 assert_eq!(Some(settings.tab_size), NonZeroU32::new(7)); @@ -789,20 +727,15 @@ async fn test_adding_worktree_discovers_external_editorconfigs(cx: &mut gpui::Te cx.executor().run_until_parked(); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((new_worktree.read(cx).id(), rel_path("file.rs")), cx) + }) + .await + .unwrap(); + cx.update(|cx| { - let tree = new_worktree.read(cx); - let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone(); - let file = File::for_entry(file_entry, new_worktree.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); + let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx); // Verify new worktree also has tab_size = 7 from shared parent editorconfig assert_eq!(Some(settings.tab_size), NonZeroU32::new(7)); @@ -943,20 +876,15 @@ async fn test_shared_external_editorconfig_cleanup_with_multiple_worktrees( assert_eq!(watcher_paths.len(), 1); }); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree_b.read(cx).id(), rel_path("file.rs")), cx) + }) + .await + .unwrap(); + cx.update(|cx| { - let tree = worktree_b.read(cx); - let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone(); - let file = File::for_entry(file_entry, worktree_b.clone()); - let file_language = project - .read(cx) - .languages() - .load_language_for_file_path(file.path.as_std_path()); - let file_language = cx - .foreground_executor() - .block_on(file_language) - .expect("Failed to get file language"); - let file = file as _; - let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned(); + let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx); // Test worktree_b still has correct settings assert_eq!(Some(settings.tab_size), NonZeroU32::new(5)); @@ -1083,26 +1011,28 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) id_base: "local worktree tasks from directory \".zed\"".into(), }; - let all_tasks = cx - .update(|cx| { - let tree = worktree.read(cx); - - let file_a = File::for_entry( - tree.entry_for_path(rel_path("a/a.rs")).unwrap().clone(), - worktree.clone(), - ) as _; - let settings_a = language_settings(None, Some(&file_a), cx); - let file_b = File::for_entry( - tree.entry_for_path(rel_path("b/b.rs")).unwrap().clone(), - worktree.clone(), - ) as _; - let settings_b = language_settings(None, Some(&file_b), cx); + let buffer_a = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path("a/a.rs")), cx) + }) + .await + .unwrap(); + let buffer_b = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), rel_path("b/b.rs")), cx) + }) + .await + .unwrap(); + cx.update(|cx| { + let settings_a = LanguageSettings::for_buffer(&buffer_a.read(cx), cx); + let settings_b = LanguageSettings::for_buffer(&buffer_b.read(cx), cx); - assert_eq!(settings_a.tab_size.get(), 8); - assert_eq!(settings_b.tab_size.get(), 2); + assert_eq!(settings_a.tab_size.get(), 8); + assert_eq!(settings_b.tab_size.get(), 2); + }); - get_all_tasks(&project, task_contexts.clone(), cx) - }) + let all_tasks = cx + .update(|cx| get_all_tasks(&project, task_contexts.clone(), cx)) .await .into_iter() .map(|(source_kind, task)| { diff --git a/crates/project/tests/integration/search.rs b/crates/project/tests/integration/search.rs index b28240289c8a5b28e4db2827e4c08b745082f4f3..79266405084d293329056b55a57c72a043aa8ff0 100644 --- a/crates/project/tests/integration/search.rs +++ b/crates/project/tests/integration/search.rs @@ -148,7 +148,7 @@ async fn test_multiline_regex(cx: &mut gpui::TestAppContext) { use language::Buffer; let text = Rope::from("hello\nworld\nhello\nworld"); let snapshot = cx - .update(|app| Buffer::build_snapshot(text, None, None, app)) + .update(|app| Buffer::build_snapshot(text, None, None, None, app)) .await; let results = search_query.search(&snapshot, None).await; diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 0f1d1e3769c405abce5ebf55818f19e64afadc82..01eb8126989668d5d6e8ea44f0313663e9d8cb4c 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -7,6 +7,7 @@ use client::{Client, UserStore}; use clock::FakeSystemClock; use collections::{HashMap, HashSet}; use language_model::LanguageModelToolResultContent; +use languages::rust_lang; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs}; @@ -14,7 +15,7 @@ use gpui::{AppContext as _, Entity, SharedString, TestAppContext}; use http_client::{BlockedHttpClient, FakeHttpClient}; use language::{ Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, - language_settings::{AllLanguageSettings, language_settings}, + language_settings::{AllLanguageSettings, LanguageSettings}, }; use lsp::{ CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT, @@ -481,6 +482,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo let worktree_id = project .update(cx, |project, cx| { + project.languages().add(rust_lang()); project.find_or_create_worktree("/code/project1", true, cx) }) .await @@ -521,9 +523,8 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo }); cx.read(|cx| { - let file = buffer.read(cx).file(); assert_eq!( - language_settings(Some("Rust".into()), file, cx).language_servers, + LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers, ["override-rust-analyzer".to_string()] ) }); @@ -646,6 +647,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext let worktree_id = project .update(cx, |project, cx| { + project.languages().add(rust_lang()); project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await @@ -668,9 +670,8 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext let fake_second_lsp = fake_second_lsp.next().await.unwrap(); cx.read(|cx| { - let file = buffer.read(cx).file(); assert_eq!( - language_settings(Some("Rust".into()), file, cx).language_servers, + LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers, ["rust-analyzer".to_string(), "fake-analyzer".to_string()] ) }); diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index abfe0ec727c7388a612c38f5bb0b0c4d0dbf5682..fda63ced4c740291554034e461b3469c8809008d 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -219,6 +219,7 @@ impl VsCodeSettings { vim_mode: None, workspace: self.workspace_settings_content(), which_key: None, + modeline_lines: None, } } diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 5762142d9bde1b1f3631f66877889d2a2bcf07e2..ea505cd2dcbd20bf5520169b808bb6848119a95a 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -204,6 +204,13 @@ pub struct SettingsContent { /// Settings related to Vim mode in Zed. pub vim: Option, + + /// Number of lines to search for modelines at the beginning and end of files. + /// Modelines contain editor directives (e.g., vim/emacs settings) that configure + /// the editor behavior for specific files. + /// + /// Default: 5 + pub modeline_lines: Option, } impl SettingsContent { diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index 13c9000b85582873f383fdb6f60927163770625b..6eb2f1dbf454ccd03492e5790a1f207f0fa47a56 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -8543,7 +8543,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { ] } - fn miscellaneous_section() -> [SettingsPageItem; 6] { + fn miscellaneous_section() -> [SettingsPageItem; 7] { [ SettingsPageItem::SectionHeader("Miscellaneous"), SettingsPageItem::SettingItem(SettingItem { @@ -8642,6 +8642,19 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { metadata: None, files: USER | PROJECT, }), + SettingsPageItem::SettingItem(SettingItem { + title: "Vim/Emacs Modeline Support", + description: "Number of lines to search for modelines (set to 0 to disable).", + field: Box::new(SettingField { + json_path: Some("modeline_lines"), + pick: |settings_content| settings_content.modeline_lines.as_ref(), + write: |settings_content, value| { + settings_content.modeline_lines = value; + }, + }), + metadata: None, + files: USER | PROJECT, + }), ] } diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index 3da0c78f8070e874187c39fcbd073994028d146a..da351ad410d078e79aa4c3038fcf88184bc648fa 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -204,19 +204,19 @@ where else { return Task::ready(Vec::new()); }; - let (file, language) = task_contexts + let (language, buffer) = task_contexts .location() .map(|location| { - let buffer = location.buffer.read(cx); + let buffer = location.buffer.clone(); ( - buffer.file().cloned(), - buffer.language_at(location.range.start), + buffer.read(cx).language_at(location.range.start), + Some(buffer), ) }) .unwrap_or_default(); task_inventory .read(cx) - .list_tasks(file, language, task_contexts.worktree(), cx) + .list_tasks(buffer, language, task_contexts.worktree(), cx) })? .await; diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 1522563d2cbeac0a2391aa30db4ab18b6522b18c..fad24930b75da256dd8adf405acebfdd8bb168f6 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -45,6 +45,7 @@ - [Debugger](./debugger.md) - [REPL](./repl.md) - [Git](./git.md) +- [Modelines](./modelines.md) # Collaboration diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index ec775964dfd35ad019a37ad58ffe42bc03c645c1..b2a8c1e88a4abbead7afe4978abd110880f1fae2 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -69,6 +69,10 @@ Settings are applied in layers: Later layers override earlier ones. For object settings (like `terminal`), properties merge rather than replace entirely. +## Per-file Settings + +Zed has some compatibility support for Emacs and Vim [modelines](./modelines.md), so you can set some settings per-file. + ## Per-Release Channel Overrides Use different settings for Stable, Preview, or Nightly builds by adding top-level channel keys: diff --git a/docs/src/modelines.md b/docs/src/modelines.md new file mode 100644 index 0000000000000000000000000000000000000000..541e44b7bae4a1fb8b4400245c2e0bf54b68dcfb --- /dev/null +++ b/docs/src/modelines.md @@ -0,0 +1,67 @@ +# Modelines + +Modelines are special comments at the beginning or end of a file that configure editor settings for that specific file. Zed supports both Vim and Emacs modeline formats, allowing you to specify settings like tab size, indentation style, and file type directly within your files. + +## Configuration + +Use the [`modeline_lines`](./reference/all-settings.md#modeline-lines) setting to control how many lines Zed searches for modelines: + +```json [settings] +{ + "modeline_lines": 5 +} +``` + +Set to `0` to disable modeline parsing entirely. + +## Emacs + +Zed has some compatibility support for [Emacs file variables](https://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html). + +Example: + +```python +# -*- mode: python; tab-width: 4; indent-tabs-mode: nil; -*- +``` + +### Supported Emacs Variables + +| Variable | Description | Zed Setting | +| -------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------ | +| `mode` | Major mode/language | Language detection | +| `tab-width` | Tab display width | [`tab_size`](./reference/all-settings.md#tab-size) | +| `fill-column` | Line wrap column | [`preferred_line_length`](./reference/all-settings.md#preferred-line-length) | +| `indent-tabs-mode` | `nil` for spaces, `t` for tabs | [`hard_tabs`](./reference/all-settings.md#hard-tabs) | +| `electric-indent-mode` | Auto-indentation | [`auto_indent`](./reference/all-settings.md#auto-indent) | +| `require-final-newline` | Ensure final newline | [`ensure_final_newline_on_save`](./reference/all-settings.md#ensure-final-newline-on-save) | +| `show-trailing-whitespace` | Show trailing whitespace | [`show_whitespaces`](./reference/all-settings.md#show-whitespaces) | + +## Vim + +Zed has some compatibility support for [Vim modeline](https://vimhelp.org/options.txt.html#modeline). + +Example: + +```python +# vim: set ft=python ts=4 sw=4 et: +``` + +### Supported Vim Options + +| Option | Aliases | Description | Zed Setting | +| -------------- | ------- | --------------------------------- | ------------------------------------------------------------------------------------------ | +| `filetype` | `ft` | File type/language | Language detection | +| `tabstop` | `ts` | Number of spaces a tab counts for | [`tab_size`](./reference/all-settings.md#tab-size) | +| `textwidth` | `tw` | Maximum line width | [`preferred_line_length`](./reference/all-settings.md#preferred-line-length) | +| `expandtab` | `et` | Use spaces instead of tabs | [`hard_tabs`](./reference/all-settings.md#hard-tabs) | +| `noexpandtab` | `noet` | Use tabs instead of spaces | [`hard_tabs`](./reference/all-settings.md#hard-tabs) | +| `autoindent` | `ai` | Enable auto-indentation | [`auto_indent`](./reference/all-settings.md#auto-indent) | +| `noautoindent` | `noai` | Disable auto-indentation | [`auto_indent`](./reference/all-settings.md#auto-indent) | +| `endofline` | `eol` | Ensure final newline | [`ensure_final_newline_on_save`](./reference/all-settings.md#ensure-final-newline-on-save) | +| `noendofline` | `noeol` | Disable final newline | [`ensure_final_newline_on_save`](./reference/all-settings.md#ensure-final-newline-on-save) | + +## Notes + +- The first kilobyte of a file is searched for modelines. +- Emacs modelines take precedence over Vim modelines when both are present. +- Modelines in the first few lines take precedence over those at the end of the file. diff --git a/typos.toml b/typos.toml index 8c57caaf0417efdb01013e76f179515d9629a47c..6788f14700086e39a394c1bff9418af7666d9b0f 100644 --- a/typos.toml +++ b/typos.toml @@ -93,6 +93,8 @@ extend-ignore-re = [ "ags", # AMD GPU Services "AGS", + # "noet" is a vim variable (ideally to ignore locally) + "noet", # Yarn Plug'n'Play "PnP" ]