diff --git a/assets/settings/default.json b/assets/settings/default.json index 75669235af0a4a0cd1128b84a0537ecc6b17b9b1..5e6f65e8b7ab1e172f9d5662e2110b0b9df42c88 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1327,6 +1327,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 268d43477e617716bcd68c062b07aa7c11b0e95d..bda1069d13225c038897f22a3748270a6b1aa0ea 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -2333,11 +2333,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 607daa8ce3a129e0f4bc53a00d1a62f479da3932..85f4f6f4eeae9cd1304d3fbde1bce05e18ddf02c 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; }; @@ -18800,7 +18800,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; @@ -19849,7 +19849,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 bc7e5b5289937d6212c662f97238e43ea185684d..656b91a25dc9d5e048d7b7a024f8dde93a36a00e 100644 --- a/crates/agent/src/tools/edit_file_tool.rs +++ b/crates/agent/src/tools/edit_file_tool.rs @@ -468,14 +468,11 @@ impl AgentTool for EditFileTool { } // If format_on_save is enabled, format the buffer - 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, - ); - settings.format_on_save != FormatOnSave::Off - }); + let format_on_save_enabled = buffer + .read_with(cx, |buffer, cx| { + let settings = language_settings::LanguageSettings::for_buffer(buffer, cx); + settings.format_on_save != FormatOnSave::Off + }); let edit_agent_output = output.await?; diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 7b6c03098f5d85a3116670c5acc34cc8cc161df8..049e7305e6b7dc38ffce798feedf27944acb7fa3 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::{cmp::Ordering, future::Future, iter, ops::Range, sync::Arc}; @@ -946,7 +946,6 @@ impl BufferDiffInner { } fn build_diff_options( - file: Option<&Arc>, language: Option, language_scope: Option, cx: &App, @@ -962,7 +961,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, @@ -1494,7 +1493,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/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index ca444024820be6b5b3a4165999d78ea9b642cc5e..b0094455c372d68dc0e8e987379da2c0a6437c3d 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -21,7 +21,7 @@ use gpui::{ App, Rgba, SharedString, TestAppContext, UpdateGlobal, VisualContext, VisualTestContext, }; use indoc::indoc; -use language::{FakeLspAdapter, language_settings::language_settings, rust_lang}; +use language::{FakeLspAdapter, language_settings::LanguageSettings, rust_lang}; use lsp::LSP_REQUEST_TIMEOUT; use pretty_assertions::assert_eq; use project::{ @@ -4020,6 +4020,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| { @@ -4032,13 +4034,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) @@ -4050,8 +4052,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)); }); @@ -4070,15 +4071,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/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 090bb9499f94662a5140927105ff1a9ce6c6c000..02a9c3b3b285a2f77b566c3bf5d026e2e6bf1575 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -12,7 +12,7 @@ use gpui::{AppContext as _, BackgroundExecutor, TestAppContext, UpdateGlobal as 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; @@ -89,6 +89,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 { @@ -118,6 +119,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(); @@ -170,9 +172,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()] ) }); @@ -1027,9 +1028,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" ) @@ -1056,9 +1056,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 862242a3d4fbc16b83f7424c4ccbe2927222424e..451d81fbd2bfc0f6fabd48546c04b0a39d8c0951 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -1333,11 +1333,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_cli/src/format_prompt.rs b/crates/edit_prediction_cli/src/format_prompt.rs index daea43a7254357e9ccc8411218070a2eb2db9568..c15c703d56c24370ff107b16543714ba8ec0277c 100644 --- a/crates/edit_prediction_cli/src/format_prompt.rs +++ b/crates/edit_prediction_cli/src/format_prompt.rs @@ -41,6 +41,7 @@ pub async fn run_format_prompt( prompt_inputs.content.as_str().into(), language, Some(app_state.languages.clone()), + None, cx, ) }); diff --git a/crates/edit_prediction_ui/src/edit_prediction_button.rs b/crates/edit_prediction_ui/src/edit_prediction_button.rs index 607f9c24def0196dd52eaa807ccdb430fd6cefdb..45f844bc09d42ebced2730c4bc3a3dc17af94df0 100644 --- a/crates/edit_prediction_ui/src/edit_prediction_button.rs +++ b/crates/edit_prediction_ui/src/edit_prediction_button.rs @@ -20,7 +20,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; @@ -668,8 +670,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, ) }); diff --git a/crates/editor/src/bracket_colorization.rs b/crates/editor/src/bracket_colorization.rs index 81d73c6725c4f543946e8394f783c9e7a30cf596..4b31b6d12185e9a8d35937ef9db6baba1373b30c 100644 --- a/crates/editor/src/bracket_colorization.rs +++ b/crates/editor/src/bracket_colorization.rs @@ -8,7 +8,7 @@ use crate::Editor; use collections::HashMap; use gpui::{Context, HighlightStyle}; use itertools::Itertools; -use language::language_settings; +use language::language_settings::LanguageSettings; use multi_buffer::{Anchor, ExcerptId}; use ui::{ActiveTheme, utils::ensure_minimum_contrast}; @@ -46,14 +46,9 @@ impl Editor { let bracket_matches_by_accent = self.visible_excerpts(false, cx).into_iter().fold( HashMap::default(), |mut acc, (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 { let fetched_chunks = self .fetched_tree_sitter_chunks .entry(excerpt_id) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6106bfaa66b5c955ef0403e72c70ef0fc6c9ef6e..541fb441a6e9417a555704b0f743ba46c2bbc52f 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::InlayId; use project::project_settings::DiagnosticSeverity; use serde::Deserialize; +use settings::Settings; use sum_tree::{Bias, TreeMap}; use text::{BufferId, LineIndent, Patch}; use ui::{SharedString, px}; @@ -1353,12 +1357,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)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 232ca9efa3943016065d79380effc7420416c1fd..37f5bd24fdb3bd6df0303383a8e6cff4fa3b3ad5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -128,8 +128,8 @@ use language::{ OutlineItem, Point, Runnable, 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, }; @@ -581,7 +581,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; @@ -5641,14 +5642,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; } @@ -5762,8 +5756,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 @@ -6648,8 +6641,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) @@ -7676,11 +7668,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; }; @@ -7695,6 +7683,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; @@ -16828,17 +16817,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, ) }); @@ -16849,7 +16838,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)| { @@ -23876,9 +23870,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 }, @@ -25147,10 +25140,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 811bc865128f17c683f186448f411be95e9a9b32..b383f547cc560f63666f04ce25ddab2d86b9f56e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -30283,10 +30283,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" ) @@ -30314,10 +30316,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/indent_guides.rs b/crates/editor/src/indent_guides.rs index 571af03a29a63e4a0ee4e42136d2e2bd6b597c95..c493876742377288c7423b57ce6d785a5b4925c2 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 4b9d2024bbb2906ceceb381cc61db09b0c9e4ddc..0e17d7455f1164453a4d91eb933b33e523a0a455 100644 --- a/crates/editor/src/inlays/inlay_hints.rs +++ b/crates/editor/src/inlays/inlay_hints.rs @@ -10,7 +10,7 @@ use futures::future::join_all; use gpui::{App, Entity, Task}; use language::{ BufferRow, - language_settings::{InlayHintKind, InlayHintSettings, language_settings}, + language_settings::{InlayHintKind, InlayHintSettings}, }; use lsp::LanguageServerId; use multi_buffer::{Anchor, ExcerptId, MultiBufferSnapshot}; @@ -36,9 +36,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 8d8f8d43b1ad1d374b130a9b4bc83297e4a76214..6d99552fd5a3b281f59ae47e0e0d83a05764fcfa 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}; @@ -323,12 +323,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/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 418abf38f3d8ca4a61403a9a3b3831c2da36c106..d684839ec2ef00af1b50d4000c4d7db79548315e 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::{LanguageSettings, language_settings}, + language_settings::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, @@ -194,6 +195,7 @@ pub struct BufferSnapshot { non_text_state_update_count: usize, tree_sitter_data: Arc, pub capability: Capability, + modeline: Option>, } /// The kind and amount of indentation in a particular line. For now, @@ -1144,6 +1146,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, @@ -1154,6 +1157,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(); @@ -1178,6 +1182,7 @@ impl Buffer { language, non_text_state_update_count: 0, capability: Capability::ReadOnly, + modeline, } } } @@ -1204,6 +1209,7 @@ impl Buffer { language: None, non_text_state_update_count: 0, capability: Capability::ReadOnly, + modeline: None, } } @@ -1234,6 +1240,7 @@ impl Buffer { language, non_text_state_update_count: 0, capability: Capability::ReadOnly, + modeline: None, } } @@ -1261,6 +1268,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(), } } @@ -1509,6 +1517,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 { @@ -2664,8 +2687,12 @@ impl Buffer { } else { // The auto-indent setting is not present in editorconfigs, hence // we can avoid passing the file here. - let auto_indent = - language_settings(language.map(|l| l.name()), None, cx).auto_indent; + let auto_indent = LanguageSettings::resolve( + None, + language.map(|l| l.name()).as_ref(), + cx, + ) + .auto_indent; previous_setting = Some((language_id, auto_indent)); auto_indent } @@ -3260,11 +3287,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 { @@ -3718,6 +3741,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() @@ -3736,11 +3764,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 { @@ -5325,6 +5349,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 80af08a53512d1d5624b20d0dad2c231f1b70a7f..fa2b1a5ab7678a7f2be606c4cf95336873845166 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 a294f1b5ae81a0d1b59ae1d685ab0d1f8fd67b5a..f6c3d075ce6b33bde1bff3c99fcb94ab081cee85 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}; @@ -136,6 +138,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![ @@ -964,6 +967,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 53906ab3bcd740191c4cf2abd497e73a5397ac31..2a4fabf63e62ecaa1b87911bf55c3a40a132d2df 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -721,6 +721,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 909eeee59f4dc0f630166a2b8af529ce6fee2d71..bb2f922ed9c29097cc55ebf602a97ed7f340d0ea 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, @@ -16,22 +18,10 @@ pub use settings::{ Formatter, FormatterList, InlayHintKind, 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>( @@ -260,6 +250,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( @@ -481,6 +539,35 @@ 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()); + settings + .auto_indent + .merge_from_option(modeline.auto_indent.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), @@ -508,22 +595,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 8d85b4f2416cad7cc7935dbb657109d5f1126aa5..a6a641929da504a82d9b4c604fb4a929e1b16de7 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", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "h++", "ipp", "inl", "ino", "ixx", "cu", "cuh", "C", "H"] +modeline_aliases = ["c++", "cpp", "cxx"] line_comments = ["// ", "/// ", "//! "] decrease_indent_patterns = [ { pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] }, diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index ed6b456b1c74d0ef1e0611e0017d240a0158a4f0..9330c31094586149e361ff813cfd47d8920766ea 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -2,7 +2,7 @@ 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::{LanguageToolchainStore, LspAdapterDelegate, LspInstaller}; @@ -544,7 +544,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 0a5122c038e1e38e0c963c3d22581f794656c276..655012944e62ffbf6cf5701457cb9f0d3b743c19 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 = ["// "] autoclose_before = ";:.,=}])>" brackets = [ 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 a6b5f9aec32eaabb90211b216fb2a9df417b178b..2701167bdb6051a5c186d2f5f9f47ae2e84a2306 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 7e7b83b3cb5ce82c614d9ba7cd9faba6d2f3a17b..5e1d8b7b4b01053b05ce2e72061b557a6a41d47b 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 10b1e49757edc106c76e0dc7c591098ebdc6723f..58362f5d0334845608f2c2624145576eb44ba72d 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", "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 d3b2dab028f627d7797feb6f710aebc83a1996d4..74bb3d4c6148761d728e7c41d52a9fd7eb7231e0 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -4,10 +4,10 @@ use async_trait::async_trait; use collections::HashMap; 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}; +use language::language_settings::LanguageSettings; +use language::{Buffer, ContextLocation, DynLspInstaller, LanguageToolchainStore, LspInstaller}; use language::{ContextProvider, LspAdapter, LspAdapterDelegate}; use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery}; use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata}; @@ -783,11 +783,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(); @@ -825,10 +824,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 @@ -935,9 +934,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 0ac1b8af015c847b96319d6466696cfeacdc24a7..6f330bf20958cb652e63109922e57a03903b01a1 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}; @@ -31,7 +31,7 @@ use util::merge_json_value_into; use util::rel_path::RelPath; use util::{ResultExt, maybe}; -use crate::language_settings::language_settings; +use crate::language_settings::LanguageSettings; pub struct RustLspAdapter; @@ -893,23 +893,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 2b2fb19c629f85c6b51eba64d154b43e716f6827..1fd53136a697775963ea778e9993fdb2569b11bb 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 283d99db4bd722d7e8a6eaf9d86d2622a191d8f8..09dfa62d708fdc2052fd9b612a2079dbe557b960 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::{ @@ -2449,10 +2450,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)) } @@ -2461,14 +2459,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, mut f: impl FnMut(&Entity)) { @@ -6080,8 +6075,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 }); @@ -6105,7 +6099,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; @@ -6201,13 +6195,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)) } @@ -6216,13 +6204,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/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index b4130b3c75e22c29108019b27665fb83a59bb0f5..6017d94a0bedcba76931e7194e65e6824244eea3 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; @@ -351,7 +351,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, @@ -500,11 +500,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 e5b91094fe670b8ed740134fc5abcebe500caf81..743de64c3c3238f96ac6b93e918744383745f7dc 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}; 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_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, @@ -2893,9 +2893,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 61c8ec10b2e66c98dbb9a4adf68dbc67c03caead..35c91a8bf21d39796a1dca79d42bf6c0d74f6a8e 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -64,10 +64,10 @@ 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, - language_settings::{FormatOnSave, Formatter, LanguageSettings, language_settings}, - point_to_lsp, + ManifestDelegate, ManifestName, ModelineSettings, Patch, PointUtf16, TextBufferSnapshot, + ToOffset, ToPointUtf16, Toolchain, Transaction, Unclipped, + language_settings::{FormatOnSave, Formatter, LanguageSettings}, + modeline, point_to_lsp, proto::{ deserialize_anchor, deserialize_anchor_range, deserialize_lsp_edit, deserialize_version, serialize_anchor, serialize_anchor_range, serialize_lsp_edit, serialize_version, @@ -1515,9 +1515,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(); (adapters_and_servers, settings) }) })?; @@ -4282,6 +4280,10 @@ impl LspStore { self.on_buffer_saved(buffer, cx); } + language::BufferEvent::Reloaded => { + self.on_buffer_reloaded(buffer, cx); + } + _ => {} } } @@ -4296,6 +4298,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); @@ -4304,6 +4307,12 @@ impl LspStore { Ok(()) } + fn on_buffer_reloaded(&mut self, buffer: Entity, cx: &mut Context) { + if self.parse_modeline(&buffer, cx) { + self.detect_language_for_buffer(&buffer, cx); + } + } + pub(crate) fn register_buffer_with_language_servers( &mut self, buffer: &Entity, @@ -4526,6 +4535,54 @@ 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.len().min(MAX_MODELINE_BYTES); + 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.len().saturating_sub(MAX_MODELINE_BYTES); + 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, @@ -4534,9 +4591,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 @@ -4581,8 +4648,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 { @@ -4889,10 +4960,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(), @@ -5466,9 +5536,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) @@ -5572,12 +5642,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(), ) }); @@ -6264,13 +6329,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())); } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 53e936d476685d96915c417f74716f814bfd061f..ab525130fa05eebc07433fe4a5d38fb09b96c48f 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -20,14 +20,14 @@ use git::{ status::{StatusCode, TrackedStatus}, }; use git2::RepositoryInitOptions; -use gpui::{App, BackgroundExecutor, FutureExt, UpdateGlobal}; +use gpui::{App, BackgroundExecutor, FutureExt, TestAppContext, UpdateGlobal}; use itertools::Itertools; use language::{ Diagnostic, DiagnosticEntry, DiagnosticEntryRef, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider, ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainList, ToolchainLister, - language_settings::{LanguageSettingsContent, language_settings}, + language_settings::{LanguageSettings, LanguageSettingsContent}, markdown_lang, rust_lang, tree_sitter_typescript, }; use lsp::{ @@ -201,48 +201,39 @@ 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_a = settings_for("a.rs"); - let settings_b = settings_for("b/b.rs"); - let settings_c = settings_for("c.js"); - let settings_readme = settings_for("README.json"); - - // .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)); - - // "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); + 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()) + }; - // README.md should not be affected by .editorconfig's globe "*.rs" - assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8)); - }); + 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_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 in b/ overrides .editorconfig in root + assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2)); + + // "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); + + // README.md should not be affected by .editorconfig's globe "*.rs" + assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8)); } #[gpui::test] @@ -276,37 +267,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] @@ -335,20 +317,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)); @@ -383,20 +360,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)); @@ -439,30 +411,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] @@ -492,20 +458,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 @@ -539,20 +500,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)); @@ -567,20 +523,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)); @@ -615,21 +566,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)); @@ -644,20 +590,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)); @@ -798,20 +739,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)); @@ -938,26 +874,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/src/search.rs b/crates/project/src/search.rs index 5710de36dce6b4cbbd8d38b3e6c3b102fe5df6c0..59150d113af22695ce7d5d3aeeda6d91cf19b4a7 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -778,7 +778,7 @@ mod tests { use language::Buffer; let text = crate::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/project/src/task_inventory.rs b/crates/project/src/task_inventory.rs index 7a63b68422bbfc98684f32ebc637151de7d1d8d9..e91bad6f13ebcda3d7d8d62c6feba04a437d7511 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}; @@ -302,17 +302,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())) }); @@ -350,19 +348,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 @@ -378,7 +375,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, @@ -394,14 +391,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 { @@ -435,7 +436,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 mut task_labels_to_ids = HashMap::>::default(); let mut lru_score = 0_u32; @@ -478,14 +479,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() @@ -1075,7 +1080,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/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 084d92c43188ecb66852b497d29afeb87507a04f..84ba3ed8c4994964390318f14961b89751a0cd7d 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, fake_provider::FakeLanguageModel}; +use languages::rust_lang; use prompt_store::ProjectContext; use extension::ExtensionHostProxy; @@ -15,7 +16,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, LanguageServerName}; use node_runtime::NodeRuntime; @@ -432,6 +433,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 @@ -472,9 +474,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()] ) }); @@ -597,6 +598,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 @@ -619,9 +621,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 c09f856aad3265bf1c6de1f7f2dc6fdac3f266bd..45f87e4d47454da646fd43c0d220c027edc1299d 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -216,6 +216,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 6923ba088b29afeaf3bc6ddbf6ca137c10f97166..6026e426c8e56559c39bf82741f39b51ee40f791 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -176,6 +176,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 dc9d1d81aed51da52e15cc030630d9d73991bac3..3e0a6f8cda401a293fe19d3b422141f19f73497b 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -8216,7 +8216,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> { ] } - fn miscellaneous_section() -> [SettingsPageItem; 6] { + fn miscellaneous_section() -> [SettingsPageItem; 7] { [ SettingsPageItem::SectionHeader("Miscellaneous"), SettingsPageItem::SettingItem(SettingItem { @@ -8315,6 +8315,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 35c8a2ee220c6dba3732ca0f323bc50eb592ce19..4a55da796da19e16520faa4bd59c7ce4f80bfb33 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 cc3bc65a56727dc8190a5ec5fe43ff8860031bab..b673b6541d662a0621570b956b23d3327ae9bd3a 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -22,6 +22,7 @@ - [Configuration](./ai/configuration.md) - [LLM Providers](./ai/llm-providers.md) - [Agent Settings](./ai/agent-settings.md) + - [Modelines](./modelines.md) - [Subscription](./ai/subscription.md) - [Models](./ai/models.md) - [Plans and Usage](./ai/plans-and-usage.md) diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index 8bbf3d881084d66316558ec57a51f62ccc7d0ab6..b43eb487b1a1d938ffe03cd040ef55657c6bc689 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -27,6 +27,7 @@ line_comments = ["# "] - `tab_size` defines the indentation/tab size used for this language (default is `4`). - `hard_tabs` whether to indent with tabs (`true`) or spaces (`false`, the default). - `first_line_pattern` is a regular expression, that in addition to `path_suffixes` (above) or `file_types` in settings can be used to match files which should use this language. For example Zed uses this to identify Shell Scripts by matching the [shebangs lines](https://github.com/zed-industries/zed/blob/main/crates/languages/src/bash/config.toml) in the first line of a script. +- `modeline_aliases` is an array of additional Emacs modes or Vim filetypes to map modeline settings to Zed language. - `debuggers` is an array of strings that are used to identify debuggers in the language. When launching a debugger's `New Process Modal`, Zed will order available debuggers by the order of entries in this array.