Detailed changes
@@ -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.
@@ -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
});
@@ -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(
@@ -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?;
@@ -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<language::BufferSnapshot> {
}
fn build_diff_options(
- file: Option<&Arc<dyn File>>,
language: Option<LanguageName>,
language_scope: Option<language::LanguageScope>,
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,
@@ -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));
});
}
@@ -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"
)
@@ -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<DebugAdapterName> =
- language::language_settings::language_settings(language_name, file, cx)
+ language::language_settings::LanguageSettings::for_buffer(buffer, cx)
.debuggers
.first()
.map(SharedString::from)
@@ -41,6 +41,7 @@ pub async fn run_format_prompt(
prompt_inputs.content.as_str().into(),
language,
Some(app_state.languages.clone()),
+ None,
cx,
)
});
@@ -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,
)
});
@@ -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)
@@ -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<MultiBuffer>, 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)]
@@ -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<Vec<(TaskSourceKind, TaskTemplate)>> {
- 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,
@@ -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"
)
@@ -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<Vec<IndentGuide>> {
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
}
@@ -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<Editor>,
) -> 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)]
@@ -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;
}
}
@@ -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()
},
},
),
@@ -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<rc::Weak<Cell<bool>>>,
+ modeline: Option<Arc<ModelineSettings>>,
_subscriptions: Vec<gpui::Subscription>,
tree_sitter_data: Arc<TreeSitterData>,
encoding: &'static Encoding,
@@ -194,6 +195,7 @@ pub struct BufferSnapshot {
non_text_state_update_count: usize,
tree_sitter_data: Arc<TreeSitterData>,
pub capability: Capability,
+ modeline: Option<Arc<ModelineSettings>>,
}
/// 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<Arc<Language>>,
language_registry: Option<Arc<LanguageRegistry>>,
+ modeline: Option<Arc<ModelineSettings>>,
cx: &mut App,
) -> impl Future<Output = BufferSnapshot> + use<> {
let entity_id = cx.reserve_entity::<Self>().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<ModelineSettings>) -> 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<ModelineSettings>> {
+ self.modeline.as_ref()
+ }
+
/// Assign the buffer a new [`Capability`].
pub fn set_capability(&mut self, capability: Capability, cx: &mut Context<Self>) {
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<T: ToOffset>(&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<ModelineSettings>> {
+ self.modeline.as_ref()
+ }
+
/// Returns the main [`Language`].
pub fn language(&self) -> Option<&Arc<Language>> {
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<T: ToOffset>(&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(),
}
}
}
@@ -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()
});
@@ -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<Arc<Language>> = 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<Regex>,
+ /// 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<String>,
}
/// The configuration for JSX tag auto-closing.
@@ -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<Self>,
+ modeline_name: &str,
+ ) -> Option<AvailableLanguage> {
+ 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<Self>,
file: &Arc<dyn File>,
@@ -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<LanguageName>,
- file: Option<&'a Arc<dyn File>>,
- 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<usize>,
+ 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::<MaxLineLen>().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<T>(target: &mut T, value: Option<T>) {
- 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 {
@@ -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<String>,
+ /// How many columns a tab should occupy.
+ pub tab_size: Option<NonZeroU32>,
+ /// Whether to indent lines using tab characters, as opposed to multiple
+ /// spaces.
+ pub hard_tabs: Option<bool>,
+ /// The number of bytes that comprise the indentation.
+ pub indent_size: Option<NonZeroU32>,
+ /// Whether to auto-indent lines.
+ pub auto_indent: Option<bool>,
+ /// The column at which to soft-wrap lines.
+ pub preferred_line_length: Option<NonZeroU32>,
+ /// Whether to ensure a final newline at the end of the file.
+ pub ensure_final_newline: Option<bool>,
+ /// Whether to show trailing whitespace on the editor.
+ pub show_trailing_whitespace: Option<bool>,
+
+ /// 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<String>)>,
+}
+
+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<ModelineSettings> {
+ 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<Regex> =
+ 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::<NonZeroU32>() {
+ settings.indent_size = Some(size);
+ }
+ }
+ "fill-column" => {
+ if let Ok(size) = value.parse::<NonZeroU32>() {
+ settings.preferred_line_length = Some(size);
+ }
+ }
+ "tab-width" => {
+ if let Ok(size) = value.parse::<NonZeroU32>() {
+ 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<Vec<Regex>> = 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<String> {
+ 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::<NonZeroU32>() {
+ settings.tab_size = Some(size);
+ }
+ }
+ "sw" | "shiftwidth" => {
+ if let Ok(size) = value.parse::<NonZeroU32>() {
+ settings.indent_size = Some(size);
+ }
+ }
+ "tw" | "textwidth" => {
+ if let Ok(size) = value.parse::<NonZeroU32>() {
+ 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::<Vec<_>>();
+ 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::<Vec<_>>();
+ 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));
+ }
+}
@@ -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<Arc<dyn File>>, _: &App) -> Task<Option<TaskTemplates>> {
+ fn associated_tasks(&self, _: Option<Entity<Buffer>>, _: &App) -> Task<Option<TaskTemplates>> {
Task::ready(None)
}
@@ -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 = "}])"
@@ -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"] },
@@ -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<Arc<dyn File>>, _: &App) -> Task<Option<TaskTemplates>> {
+ fn associated_tasks(&self, _: Option<Entity<Buffer>>, _: &App) -> Task<Option<TaskTemplates>> {
let package_cwd = if GO_PACKAGE_TASK_VARIABLE.template_value() == "." {
None
} else {
@@ -1,6 +1,7 @@
name = "Go"
grammar = "go"
path_suffixes = ["go"]
+modeline_aliases = ["golang"]
line_comments = ["// "]
autoclose_before = ";:.,=}])>"
brackets = [
@@ -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 = ["// "]
@@ -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<Arc<dyn language::File>>,
+ buffer: Option<Entity<Buffer>>,
cx: &App,
) -> gpui::Task<Option<TaskTemplates>> {
- 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());
@@ -51,6 +51,7 @@ pub static LANGUAGE_GIT_COMMIT: std::sync::LazyLock<Arc<Language>> =
matcher: LanguageMatcher {
path_suffixes: vec!["COMMIT_EDITMSG".to_owned()],
first_line_pattern: None,
+ ..LanguageMatcher::default()
},
line_comments: vec![Arc::from("#")],
..LanguageConfig::default()
@@ -1,6 +1,7 @@
name = "Markdown"
grammar = "markdown"
path_suffixes = ["md", "mdx", "mdwn", "markdown", "MD"]
+modeline_aliases = ["md"]
completion_query_characters = ["-"]
block_comment = { start = "<!--", prefix = "", end = "-->", tab_size = 0 }
autoclose_before = ";:.,=}])>"
@@ -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<dyn LanguageToolchainStore>,
cx: &mut gpui::App,
) -> Task<Result<task::TaskVariables>> {
- 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<Arc<dyn language::File>>,
+ buffer: Option<Entity<Buffer>>,
cx: &App,
) -> Task<Option<TaskTemplates>> {
- 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<dyn language::File>>, cx: &App) -> TestRunner {
+fn selected_test_runner(location: Option<&Entity<Buffer>>, 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)
@@ -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 = [
@@ -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<Arc<dyn language::File>>,
+ buffer: Option<Entity<Buffer>>,
cx: &App,
) -> Task<Option<TaskTemplates>> {
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 {
@@ -1,6 +1,7 @@
name = "Rust"
grammar = "rust"
path_suffixes = ["rs"]
+modeline_aliases = ["rs", "rustic"]
line_comments = ["// ", "/// ", "//! "]
autoclose_before = ";:.,=}])>"
brackets = [
@@ -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 }
@@ -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<Arc<dyn File>>,
+ buffer: Option<Entity<Buffer>>,
cx: &App,
) -> Task<Option<TaskTemplates>> {
- 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 {
@@ -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 }
@@ -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 = [
@@ -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<Buffer>)) {
@@ -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<T: ToOffset>(&self, point: T) -> Option<LanguageScope> {
@@ -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(),
@@ -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 {
@@ -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::<Vec<_>>();
- 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<Buffer>, cx: &mut Context<Self>) {
+ 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<Buffer>,
@@ -4526,6 +4535,54 @@ impl LspStore {
})
}
+ fn parse_modeline(&mut self, buffer_handle: &Entity<Buffer>, cx: &mut Context<Self>) -> bool {
+ let buffer = buffer_handle.read(cx);
+ let content = buffer.as_rope();
+
+ let modeline_settings = {
+ let settings_store = cx.global::<SettingsStore>();
+ 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<Buffer>,
@@ -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<Result<Option<Transaction>>> {
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()));
}
@@ -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)| {
@@ -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;
@@ -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<Option<TaskTemplate>> {
- 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<Arc<dyn File>>,
+ buffer: Option<Entity<Buffer>>,
language: Option<Arc<Language>>,
worktree: Option<WorktreeId>,
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::<String, HashSet<TaskId>>::default();
let mut lru_score = 0_u32;
@@ -478,14 +479,18 @@ impl Inventory {
let global_tasks = self.global_templates_from_settings().collect::<Vec<_>>();
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<Arc<dyn File>>, _: &App) -> Task<Option<TaskTemplates>> {
+ fn associated_tasks(&self, _: Option<Entity<Buffer>>, _: &App) -> Task<Option<TaskTemplates>> {
Task::ready(Some(self.templates.clone()))
}
}
@@ -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()]
)
});
@@ -216,6 +216,7 @@ impl VsCodeSettings {
vim_mode: None,
workspace: self.workspace_settings_content(),
which_key: None,
+ modeline_lines: None,
}
}
@@ -176,6 +176,13 @@ pub struct SettingsContent {
/// Settings related to Vim mode in Zed.
pub vim: Option<VimSettingsContent>,
+
+ /// 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<usize>,
}
impl SettingsContent {
@@ -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,
+ }),
]
}
@@ -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;
@@ -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)
@@ -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.
<!--
@@ -0,0 +1,67 @@
+# Modelines
+
+Modelines are special comments at the beginning or end of a file that configure editor settings for that specific file. Zed supports both Vim and Emacs modeline formats, allowing you to specify settings like tab size, indentation style, and file type directly within your files.
+
+## Configuration
+
+Use the [`modeline_lines`](./configuring-zed.md#modeline-lines) setting to control how many lines Zed searches for modelines:
+
+```json [settings]
+{
+ "modeline_lines": 5
+}
+```
+
+Set to `0` to disable modeline parsing entirely.
+
+## Emacs
+
+Zed has some compatibility support for [Emacs file variables](https://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html).
+
+Example:
+
+```python
+# -*- mode: python; tab-width: 4; indent-tabs-mode: nil; -*-
+```
+
+### Supported Emacs Variables
+
+| Variable | Description | Zed Setting |
+| -------------------------- | ------------------------------ | --------------------------------------------------------------------- |
+| `mode` | Major mode/language | Language detection |
+| `tab-width` | Tab display width | [`tab_size`](./configuring-zed.md#tab-size) |
+| `fill-column` | Line wrap column | [`preferred_line_length`](./configuring-zed.md#preferred-line-length) |
+| `indent-tabs-mode` | `nil` for spaces, `t` for tabs | [`hard_tabs`](./configuring-zed.md#hard-tabs) |
+| `electric-indent-mode` | Auto-indentation | [`auto_indent`](./configuring-zed.md#auto-indent) |
+| `require-final-newline` | Ensure final newline | [`ensure_final_newline`](./configuring-zed.md#ensure-final-newline) |
+| `show-trailing-whitespace` | Show trailing whitespace | [`show_whitespaces`](./configuring-zed.md#show-whitespaces) |
+
+## Vim
+
+Zed has some compatibility support for [Vim modeline](https://vimhelp.org/options.txt.html#modeline).
+
+Example:
+
+```python
+# vim: set ft=python ts=4 sw=4 et:
+```
+
+### Supported Vim Options
+
+| Option | Aliases | Description | Zed Setting |
+| -------------- | ------- | --------------------------------- | --------------------------------------------------------------------- |
+| `filetype` | `ft` | File type/language | Language detection |
+| `tabstop` | `ts` | Number of spaces a tab counts for | [`tab_size`](./configuring-zed.md#tab-size) |
+| `textwidth` | `tw` | Maximum line width | [`preferred_line_length`](./configuring-zed.md#preferred-line-length) |
+| `expandtab` | `et` | Use spaces instead of tabs | [`hard_tabs`](./configuring-zed.md#hard-tabs) |
+| `noexpandtab` | `noet` | Use tabs instead of spaces | [`hard_tabs`](./configuring-zed.md#hard-tabs) |
+| `autoindent` | `ai` | Enable auto-indentation | [`auto_indent`](./configuring-zed.md#auto-indent) |
+| `noautoindent` | `noai` | Disable auto-indentation | [`auto_indent`](./configuring-zed.md#auto-indent) |
+| `endofline` | `eol` | Ensure final newline | [`ensure_final_newline`](./configuring-zed.md#ensure-final-newline) |
+| `noendofline` | `noeol` | Disable final newline | [`ensure_final_newline`](./configuring-zed.md#ensure-final-newline) |
+
+## Notes
+
+- The first kilobyte of a file is searched for modelines.
+- Emacs modelines take precedence over Vim modelines when both are present.
+- Modelines in the first few lines take precedence over those at the end of the file.
@@ -82,6 +82,8 @@ extend-ignore-re = [
# AMD GPU Services
"ags",
# AMD GPU Services
- "AGS"
+ "AGS",
+ # "noet" is a vim variable (ideally to ignore locally)
+ "noet",
]
check-filename = true