Add vim/emacs modeline support (#49267)

Many editors such as vim and emacs support "modelines", a comment at the
beginning of the file that allows the file type to be explicitly
specified along with per-file specific settings

- The amount of configurations, style and settings mapping cannot be
handled in one go, so this opens up a lot of potential improvements.
- I left out the possiblity to have "zed" specific modelines for now,
but this could be potentially interesting.
- Mapping the mode or filetype to zed language names isn't obvious
either. We may want to make it configurable.

This is my first contribution to zed, be kind. I struggled a bit to find
the right place to add those settings. I use a similar approach as done
with editorconfig (merge_with_editorconfig). There might be better ways.

Closes #4762

Release Notes:

- Add basic emacs/vim modeline support.

Supersedes #41899, changes:
- limit reading to the first and last 1kb
- add documentation
- more variables handled
- add Arc around ModelineSettings to avoid extra cloning
- changed the way mode -> language mapping is done, thanks to
`modeline_aliases` language config
- drop vim ex: support
- made "Local Variables:" handling a separate commit, so we can drop it
easily
- various code style improvements

---------

Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Marc-Andre Lureau 2026-03-25 07:15:51 +04:00 committed by GitHub
parent 34807eb146
commit 58fec75396
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 1549 additions and 597 deletions

View file

@ -1346,6 +1346,12 @@
"hard_tabs": false,
// How many columns a tab should occupy.
"tab_size": 4,
// Number of lines to search for modelines at the beginning and end of files.
// Modelines contain editor directives (e.g., vim/emacs settings) that configure
// the editor behavior for specific files.
//
// A value of 0 disables modelines support.
"modeline_lines": 5,
// What debuggers are preferred by default for all languages.
"debuggers": [],
// Whether to enable word diff highlighting in the editor.

View file

@ -2588,11 +2588,8 @@ impl AcpThread {
let format_on_save = buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
let settings = language::language_settings::language_settings(
buffer.language().map(|l| l.name()),
buffer.file(),
cx,
);
let settings =
language::language_settings::LanguageSettings::for_buffer(buffer, cx);
settings.format_on_save != FormatOnSave::Off
});

View file

@ -550,7 +550,7 @@ impl Default for EditorStyle {
}
pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle {
let show_background = language_settings::language_settings(None, None, cx)
let show_background = language_settings::language_settings(cx).get()
.inlay_hints
.show_background;
@ -5989,7 +5989,7 @@ impl Editor {
let file = buffer.file();
if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions {
if !language_settings(cx).buffer(buffer).get().show_edit_predictions {
return EditPredictionSettings::Disabled;
};
@ -18801,7 +18801,7 @@ fn choose_completion_range(
} = &completion.source
{
let completion_mode_setting =
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
language_settings(cx).buffer(buffer).get()
.completions
.lsp_insert_mode;
@ -19850,7 +19850,7 @@ fn inlay_hint_settings(
) -> InlayHintSettings {
let file = snapshot.file_at(location);
let language = snapshot.language_at(location).map(|l| l.name());
language_settings(language, file, cx).inlay_hints
language_settings(cx).language(language).file(file).get().inlay_hints
}
fn consume_contiguous_rows(

View file

@ -419,17 +419,6 @@ impl AgentTool for EditFileTool {
EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges,
EditAgentOutputEvent::ResolvingEditRange(range) => {
diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx));
// if !emitted_location {
// let line = buffer.update(cx, |buffer, _cx| {
// range.start.to_point(&buffer.snapshot()).row
// }).ok();
// if let Some(abs_path) = abs_path.clone() {
// event_stream.update_fields(ToolCallUpdateFields {
// locations: Some(vec![ToolCallLocation { path: abs_path, line }]),
// ..Default::default()
// });
// }
// }
}
}
}
@ -437,11 +426,7 @@ impl AgentTool for EditFileTool {
output.await?;
let format_on_save_enabled = buffer.read_with(cx, |buffer, cx| {
let settings = language_settings::language_settings(
buffer.language().map(|l| l.name()),
buffer.file(),
cx,
);
let settings = language_settings::LanguageSettings::for_buffer(buffer, cx);
settings.format_on_save != FormatOnSave::Off
});

View file

@ -626,11 +626,7 @@ impl EditSession {
}
let format_on_save_enabled = self.buffer.read_with(cx, |buffer, cx| {
let settings = language_settings::language_settings(
buffer.language().map(|l| l.name()),
buffer.file(),
cx,
);
let settings = language_settings::LanguageSettings::for_buffer(buffer, cx);
settings.format_on_save != FormatOnSave::Off
});

View file

@ -2,8 +2,8 @@ use futures::channel::oneshot;
use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task};
use language::{
Capability, Diff, DiffOptions, File, Language, LanguageName, LanguageRegistry,
language_settings::language_settings, word_diff_ranges,
Capability, Diff, DiffOptions, Language, LanguageName, LanguageRegistry,
language_settings::LanguageSettings, word_diff_ranges,
};
use rope::Rope;
use std::{
@ -1067,7 +1067,6 @@ impl BufferDiffInner<language::BufferSnapshot> {
}
fn build_diff_options(
file: Option<&Arc<dyn File>>,
language: Option<LanguageName>,
language_scope: Option<language::LanguageScope>,
cx: &App,
@ -1083,7 +1082,7 @@ fn build_diff_options(
}
}
language_settings(language, file, cx)
LanguageSettings::resolve(None, language.as_ref(), cx)
.word_diff_enabled
.then_some(DiffOptions {
language_scope,
@ -1656,7 +1655,6 @@ impl BufferDiff {
let base_text_changed = base_text_change.is_some();
let compute_base_text_edits = base_text_change == Some(true);
let diff_options = build_diff_options(
None,
language.as_ref().map(|l| l.name()),
language.as_ref().map(|l| l.default_scope()),
cx,

View file

@ -23,7 +23,7 @@ use gpui::{
VisualTestContext,
};
use indoc::indoc;
use language::{FakeLspAdapter, language_settings::language_settings, rust_lang};
use language::{FakeLspAdapter, language_settings::LanguageSettings, rust_lang};
use lsp::DEFAULT_LSP_REQUEST_TIMEOUT;
use multi_buffer::{AnchorRangeExt as _, MultiBufferRow};
use pretty_assertions::assert_eq;
@ -4036,6 +4036,8 @@ async fn test_collaborating_with_external_editorconfig(
.await
.unwrap();
project_a.update(cx_a, |project, _| project.languages().add(rust_lang()));
// Open buffer on client A
let buffer_a = project_a
.update(cx_a, |p, cx| {
@ -4048,13 +4050,13 @@ async fn test_collaborating_with_external_editorconfig(
// Verify client A sees external editorconfig settings
cx_a.read(|cx| {
let file = buffer_a.read(cx).file();
let settings = language_settings(Some("Rust".into()), file, cx);
let settings = LanguageSettings::for_buffer(&buffer_a.read(cx), cx);
assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
});
// Client B joins the project
let project_b = client_b.join_remote_project(project_id, cx_b).await;
project_b.update(cx_b, |project, _| project.languages().add(rust_lang()));
let buffer_b = project_b
.update(cx_b, |p, cx| {
p.open_buffer((worktree_id, rel_path("src/main.rs")), cx)
@ -4066,8 +4068,7 @@ async fn test_collaborating_with_external_editorconfig(
// Verify client B also sees external editorconfig settings
cx_b.read(|cx| {
let file = buffer_b.read(cx).file();
let settings = language_settings(Some("Rust".into()), file, cx);
let settings = LanguageSettings::for_buffer(&buffer_b.read(cx), cx);
assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
});
@ -4086,15 +4087,13 @@ async fn test_collaborating_with_external_editorconfig(
// Verify client A sees updated settings
cx_a.read(|cx| {
let file = buffer_a.read(cx).file();
let settings = language_settings(Some("Rust".into()), file, cx);
let settings = LanguageSettings::for_buffer(&buffer_a.read(cx), cx);
assert_eq!(Some(settings.tab_size), NonZeroU32::new(9));
});
// Verify client B also sees updated settings
cx_b.read(|cx| {
let file = buffer_b.read(cx).file();
let settings = language_settings(Some("Rust".into()), file, cx);
let settings = LanguageSettings::for_buffer(&buffer_b.read(cx), cx);
assert_eq!(Some(settings.tab_size), NonZeroU32::new(9));
});
}

View file

@ -14,7 +14,7 @@ use gpui::{
use http_client::BlockedHttpClient;
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
language_settings::{Formatter, FormatterList, language_settings},
language_settings::{Formatter, FormatterList, LanguageSettings},
rust_lang, tree_sitter_typescript,
};
use node_runtime::NodeRuntime;
@ -91,6 +91,7 @@ async fn test_sharing_an_ssh_remote_project(
let remote_http_client = Arc::new(BlockedHttpClient);
let node = NodeRuntime::unavailable();
let languages = Arc::new(LanguageRegistry::new(server_cx.executor()));
languages.add(rust_lang());
let _headless_project = server_cx.new(|cx| {
HeadlessProject::new(
HeadlessAppState {
@ -121,6 +122,7 @@ async fn test_sharing_an_ssh_remote_project(
// User B joins the project.
let project_b = client_b.join_remote_project(project_id, cx_b).await;
project_b.update(cx_b, |project, _| project.languages().add(rust_lang()));
let worktree_b = project_b
.update(cx_b, |project, cx| project.worktree_for_id(worktree_id, cx))
.unwrap();
@ -173,9 +175,8 @@ async fn test_sharing_an_ssh_remote_project(
executor.run_until_parked();
cx_b.read(|cx| {
let file = buffer_b.read(cx).file();
assert_eq!(
language_settings(Some("Rust".into()), file, cx).language_servers,
LanguageSettings::for_buffer(buffer_b.read(cx), cx).language_servers,
["override-rust-analyzer".to_string()]
)
});
@ -1284,9 +1285,8 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
let fake_language_server = fake_language_servers.next();
cx_a.read(|cx| {
let file = buffer_before_approval.read(cx).file();
assert_eq!(
language_settings(Some("Rust".into()), file, cx).language_servers,
LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers,
["...".to_string()],
"remote .zed/settings.json must not sync before trust approval"
)
@ -1313,9 +1313,8 @@ async fn test_ssh_remote_worktree_trust(cx_a: &mut TestAppContext, server_cx: &m
cx_a.run_until_parked();
cx_a.read(|cx| {
let file = buffer_before_approval.read(cx).file();
assert_eq!(
language_settings(Some("Rust".into()), file, cx).language_servers,
LanguageSettings::for_buffer(buffer_before_approval.read(cx), cx).language_servers,
["override-rust-analyzer".to_string()],
"remote .zed/settings.json should sync after trust approval"
)

View file

@ -1337,11 +1337,10 @@ impl PickerDelegate for DebugDelegate {
else {
return;
};
let file = location.buffer.read(cx).file();
let language = location.buffer.read(cx).language();
let language_name = language.as_ref().map(|l| l.name());
let buffer = location.buffer.read(cx);
let language = buffer.language();
let Some(adapter): Option<DebugAdapterName> =
language::language_settings::language_settings(language_name, file, cx)
language::language_settings::LanguageSettings::for_buffer(buffer, cx)
.debuggers
.first()
.map(SharedString::from)

View file

@ -18,7 +18,9 @@ use gpui::{
use indoc::indoc;
use language::{
EditPredictionsMode, File, Language,
language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings},
language_settings::{
AllLanguageSettings, EditPredictionProvider, LanguageSettings, all_language_settings,
},
};
use project::{DisableAiSettings, Project};
use regex::Regex;
@ -674,8 +676,7 @@ impl EditPredictionButton {
let language_state = self.language.as_ref().map(|language| {
(
language.clone(),
language_settings::language_settings(Some(language.name()), None, cx)
.show_edit_predictions,
LanguageSettings::resolve(None, Some(&language.name()), cx).show_edit_predictions,
)
});
@ -1599,8 +1600,7 @@ fn emit_edit_prediction_menu_opened(
) {
let language_name = language.as_ref().map(|l| l.name());
let edit_predictions_enabled_for_language =
language_settings::language_settings(language_name, file.as_ref(), cx)
.show_edit_predictions;
LanguageSettings::resolve(None, language_name.as_ref(), cx).show_edit_predictions;
let file_extension = file
.as_ref()
.and_then(|f| {

View file

@ -8,7 +8,7 @@ use crate::{Editor, HighlightKey};
use collections::{HashMap, HashSet};
use gpui::{AppContext as _, Context, HighlightStyle};
use itertools::Itertools;
use language::{BufferRow, BufferSnapshot, language_settings};
use language::{BufferRow, BufferSnapshot, language_settings::LanguageSettings};
use multi_buffer::{Anchor, ExcerptId};
use ui::{ActiveTheme, utils::ensure_minimum_contrast};
@ -29,14 +29,9 @@ impl Editor {
let excerpt_data: Vec<(ExcerptId, BufferSnapshot, Range<usize>)> = visible_excerpts
.into_iter()
.filter_map(|(excerpt_id, (buffer, _, buffer_range))| {
let buffer_snapshot = buffer.read(cx).snapshot();
if language_settings::language_settings(
buffer_snapshot.language().map(|language| language.name()),
buffer_snapshot.file(),
cx,
)
.colorize_brackets
{
let buffer = buffer.read(cx);
let buffer_snapshot = buffer.snapshot();
if LanguageSettings::for_buffer(&buffer, cx).colorize_brackets {
Some((excerpt_id, buffer_snapshot, buffer_range))
} else {
None

View file

@ -97,7 +97,10 @@ use gpui::{
App, Context, Entity, EntityId, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle,
WeakEntity,
};
use language::{Point, Subscription as BufferSubscription, language_settings::language_settings};
use language::{
Point, Subscription as BufferSubscription,
language_settings::{AllLanguageSettings, LanguageSettings},
};
use multi_buffer::{
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16,
MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
@ -105,6 +108,7 @@ use multi_buffer::{
use project::project_settings::DiagnosticSeverity;
use project::{InlayId, lsp_store::LspFoldingRange, lsp_store::TokenType};
use serde::Deserialize;
use settings::Settings;
use smallvec::SmallVec;
use sum_tree::{Bias, TreeMap};
use text::{BufferId, LineIndent, Patch};
@ -1443,12 +1447,11 @@ impl DisplayMap {
#[instrument(skip_all)]
fn tab_size(buffer: &Entity<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)]
@ -1667,11 +1670,7 @@ impl DisplaySnapshot {
else {
return false;
};
let settings = language_settings(
buffer_snapshot.language().map(|l| l.name()),
buffer_snapshot.file(),
cx,
);
let settings = LanguageSettings::for_buffer_snapshot(&buffer_snapshot, None, cx);
settings.semantic_tokens.use_tree_sitter()
}

View file

@ -5,7 +5,7 @@ use futures::FutureExt;
use futures::future::join_all;
use gpui::{App, Context, HighlightStyle, Task};
use itertools::Itertools as _;
use language::language_settings::language_settings;
use language::language_settings::LanguageSettings;
use language::{Buffer, OutlineItem};
use multi_buffer::{
Anchor, AnchorRangeExt as _, MultiBufferOffset, MultiBufferRow, MultiBufferSnapshot,
@ -239,7 +239,7 @@ impl Editor {
}
fn lsp_symbols_enabled(buffer: &Buffer, cx: &App) -> bool {
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
LanguageSettings::for_buffer(buffer, cx)
.document_symbols
.lsp_enabled()
}

View file

@ -136,8 +136,8 @@ use language::{
OutlineItem, Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
WordsQuery,
language_settings::{
self, LanguageSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
all_language_settings, language_settings,
self, AllLanguageSettings, LanguageSettings, LspInsertMode, RewrapBehavior,
WordsCompletionMode, all_language_settings,
},
point_from_lsp, point_to_lsp, text_diff_with_options,
};
@ -596,7 +596,8 @@ impl Default for EditorStyle {
}
pub fn make_inlay_hints_style(cx: &App) -> HighlightStyle {
let show_background = language_settings::language_settings(None, None, cx)
let show_background = AllLanguageSettings::get_global(cx)
.defaults
.inlay_hints
.show_background;
@ -5977,14 +5978,7 @@ impl Editor {
.read(cx)
.text_anchor_for_position(position, cx)?;
let settings = language_settings::language_settings(
buffer
.read(cx)
.language_at(buffer_position)
.map(|l| l.name()),
buffer.read(cx).file(),
cx,
);
let settings = LanguageSettings::for_buffer_at(&buffer.read(cx), buffer_position, cx);
if !settings.use_on_type_format {
return None;
}
@ -6098,8 +6092,7 @@ impl Editor {
let language = buffer_snapshot
.language_at(buffer_position.text_anchor)
.map(|language| language.name());
let language_settings = language_settings(language.clone(), buffer_snapshot.file(), cx);
let language_settings = multibuffer_snapshot.language_settings_at(buffer_position, cx);
let completion_settings = language_settings.completions.clone();
let show_completions_on_input = self
@ -6967,8 +6960,7 @@ impl Editor {
let resolved_tasks = resolved_tasks.as_ref()?;
let buffer = buffer.read(cx);
let language = buffer.language()?;
let file = buffer.file();
let debug_adapter = language_settings(language.name().into(), file, cx)
let debug_adapter = LanguageSettings::for_buffer(&buffer, cx)
.debuggers
.first()
.map(SharedString::from)
@ -8066,11 +8058,7 @@ impl Editor {
return EditPredictionSettings::Disabled;
}
let buffer = buffer.read(cx);
let file = buffer.file();
if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions {
if !LanguageSettings::for_buffer(&buffer.read(cx), cx).show_edit_predictions {
return EditPredictionSettings::Disabled;
};
@ -8085,6 +8073,7 @@ impl Editor {
.as_ref()
.is_some_and(|provider| provider.provider.show_predictions_in_menu());
let file = buffer.read(cx).file();
let preview_requires_modifier =
all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle;
@ -24484,9 +24473,8 @@ impl Editor {
|mut acc, buffer| {
let buffer = buffer.read(cx);
let language = buffer.language().map(|language| language.name());
if let hash_map::Entry::Vacant(v) = acc.entry(language.clone()) {
let file = buffer.file();
v.insert(language_settings(language, file, cx).into_owned());
if let hash_map::Entry::Vacant(v) = acc.entry(language) {
v.insert(LanguageSettings::for_buffer(&buffer, cx).into_owned());
}
acc
},
@ -25988,10 +25976,9 @@ fn process_completion_for_edit(
CompletionIntent::CompleteWithInsert => false,
CompletionIntent::CompleteWithReplace => true,
CompletionIntent::Complete | CompletionIntent::Compose => {
let insert_mode =
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
.completions
.lsp_insert_mode;
let insert_mode = LanguageSettings::for_buffer(&buffer, cx)
.completions
.lsp_insert_mode;
match insert_mode {
LspInsertMode::Insert => false,
LspInsertMode::Replace => true,

View file

@ -32725,10 +32725,12 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
let fake_language_server = fake_language_servers.next();
cx.read(|cx| {
let file = buffer_before_approval.read(cx).file();
assert_eq!(
language::language_settings::language_settings(Some("Rust".into()), file, cx)
.language_servers,
language::language_settings::LanguageSettings::for_buffer(
buffer_before_approval.read(cx),
cx
)
.language_servers,
["...".to_string()],
"local .zed/settings.json must not apply before trust approval"
)
@ -32756,10 +32758,12 @@ async fn test_local_worktree_trust(cx: &mut TestAppContext) {
cx.run_until_parked();
cx.read(|cx| {
let file = buffer_before_approval.read(cx).file();
assert_eq!(
language::language_settings::language_settings(Some("Rust".into()), file, cx)
.language_servers,
language::language_settings::LanguageSettings::for_buffer(
buffer_before_approval.read(cx),
cx
)
.language_servers,
["override-rust-analyzer".to_string()],
"local .zed/settings.json should apply after trust approval"
)

View file

@ -1,6 +1,6 @@
use futures::future::join_all;
use itertools::Itertools;
use language::language_settings::language_settings;
use language::language_settings::LanguageSettings;
use text::BufferId;
use ui::{Context, Window};
@ -29,13 +29,9 @@ impl Editor {
let id = buffer.read(cx).remote_id();
(for_buffer.is_none_or(|target| target == id))
&& self.registered_buffers.contains_key(&id)
&& language_settings(
buffer.read(cx).language().map(|l| l.name()),
buffer.read(cx).file(),
cx,
)
.document_folding_ranges
.enabled()
&& LanguageSettings::for_buffer(buffer.read(cx), cx)
.document_folding_ranges
.enabled()
})
.unique_by(|buffer| buffer.read(cx).remote_id())
.collect::<Vec<_>>();
@ -104,7 +100,7 @@ impl Editor {
.into_iter()
.filter(|buffer| {
let buffer = buffer.read(cx);
!language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
!LanguageSettings::for_buffer(&buffer, cx)
.document_folding_ranges
.enabled()
})

View file

@ -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
}

View file

@ -11,7 +11,7 @@ use gpui::{App, Entity, Pixels, Task};
use itertools::Itertools;
use language::{
BufferRow,
language_settings::{InlayHintKind, InlayHintSettings, language_settings},
language_settings::{InlayHintKind, InlayHintSettings},
};
use lsp::LanguageServerId;
use multi_buffer::{Anchor, ExcerptId, MultiBufferSnapshot};
@ -38,9 +38,7 @@ pub fn inlay_hint_settings(
snapshot: &MultiBufferSnapshot,
cx: &mut Context<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)]

View file

@ -5,7 +5,7 @@ use multi_buffer::{BufferOffset, MultiBuffer, ToOffset};
use std::ops::Range;
use util::ResultExt as _;
use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node};
use language::{BufferSnapshot, JsxTagAutoCloseConfig, Node, language_settings::LanguageSettings};
use text::{Anchor, OffsetRangeExt as _};
use crate::{Editor, SelectionEffects};
@ -322,12 +322,10 @@ pub(crate) fn refresh_enabled_in_any_buffer(
if language.config().jsx_tag_auto_close.is_none() {
continue;
}
let language_settings = language::language_settings::language_settings(
Some(language.name()),
snapshot.file(),
cx,
);
if language_settings.jsx_tag_auto_close {
let should_auto_close =
LanguageSettings::resolve(Some(buffer), Some(&language.name()), cx)
.jsx_tag_auto_close;
if should_auto_close {
found_enabled = true;
}
}

View file

@ -637,17 +637,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,
)
});
@ -658,7 +658,12 @@ impl Editor {
if let Some(inventory) = inventory {
for RunnableTag(tag) in tags {
let new_tasks = inventory.update(cx, |inventory, cx| {
inventory.list_tasks(file.clone(), Some(language.clone()), worktree_id, cx)
inventory.list_tasks(
buffer.clone(),
Some(language.clone()),
worktree_id,
cx,
)
});
templates_with_tags.extend(new_tasks.await.into_iter().filter(
move |(_, template)| {
@ -715,7 +720,7 @@ mod tests {
use std::{sync::Arc, time::Duration};
use futures::StreamExt as _;
use gpui::{AppContext as _, Task, TestAppContext};
use gpui::{AppContext as _, Entity, Task, TestAppContext};
use indoc::indoc;
use language::{ContextProvider, FakeLspAdapter};
use languages::rust_lang;
@ -742,7 +747,7 @@ mod tests {
impl ContextProvider for TestRustContextProvider {
fn associated_tasks(
&self,
_: Option<Arc<dyn language::File>>,
_: Option<Entity<language::Buffer>>,
_: &gpui::App,
) -> Task<Option<TaskTemplates>> {
Task::ready(Some(TaskTemplates(vec![
@ -769,7 +774,7 @@ mod tests {
impl ContextProvider for TestRustContextProviderWithLsp {
fn associated_tasks(
&self,
_: Option<Arc<dyn language::File>>,
_: Option<Entity<language::Buffer>>,
_: &gpui::App,
) -> Task<Option<TaskTemplates>> {
Task::ready(Some(TaskTemplates(vec![TaskTemplate {

View file

@ -6,7 +6,7 @@ use gpui::{
App, Context, FontStyle, FontWeight, HighlightStyle, StrikethroughStyle, Task, UnderlineStyle,
};
use itertools::Itertools;
use language::language_settings::language_settings;
use language::language_settings::LanguageSettings;
use project::{
lsp_store::{
BufferSemanticToken, BufferSemanticTokens, RefreshForServer, SemanticTokenStylizer,
@ -155,13 +155,9 @@ impl Editor {
.filter_map(|editor_buffer| {
let editor_buffer_id = editor_buffer.read(cx).remote_id();
if self.registered_buffers.contains_key(&editor_buffer_id)
&& language_settings(
editor_buffer.read(cx).language().map(|l| l.name()),
editor_buffer.read(cx).file(),
cx,
)
.semantic_tokens
.enabled()
&& LanguageSettings::for_buffer(editor_buffer.read(cx), cx)
.semantic_tokens
.enabled()
{
Some((editor_buffer_id, editor_buffer))
} else {
@ -184,7 +180,7 @@ impl Editor {
.buffer(*buffer_id)
.is_some_and(|buffer| {
let buffer = buffer.read(cx);
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
LanguageSettings::for_buffer(&buffer, cx)
.semantic_tokens
.enabled()
})

View file

@ -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()
},
},
),

View file

@ -1,10 +1,10 @@
pub mod row_chunk;
use crate::{
DebuggerTextObject, LanguageScope, Outline, OutlineConfig, PLAIN_TEXT, RunnableCapture,
RunnableTag, TextObject, TreeSitterOptions,
DebuggerTextObject, LanguageScope, ModelineSettings, Outline, OutlineConfig, PLAIN_TEXT,
RunnableCapture, RunnableTag, TextObject, TreeSitterOptions,
diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup},
language_settings::{AutoIndentMode, LanguageSettings, language_settings},
language_settings::{AutoIndentMode, LanguageSettings},
outline::OutlineItem,
row_chunk::RowChunks,
syntax_map::{
@ -135,6 +135,7 @@ pub struct Buffer {
/// The contents of a cell are (self.version, has_changes) at the time of a last call.
has_unsaved_edits: Cell<(clock::Global, bool)>,
change_bits: Vec<rc::Weak<Cell<bool>>>,
modeline: Option<Arc<ModelineSettings>>,
_subscriptions: Vec<gpui::Subscription>,
tree_sitter_data: Arc<TreeSitterData>,
encoding: &'static Encoding,
@ -195,6 +196,7 @@ pub struct BufferSnapshot {
file: Option<Arc<dyn File>>,
non_text_state_update_count: usize,
pub capability: Capability,
modeline: Option<Arc<ModelineSettings>>,
}
/// The kind and amount of indentation in a particular line. For now,
@ -1163,6 +1165,7 @@ impl Buffer {
deferred_ops: OperationQueue::new(),
has_conflict: false,
change_bits: Default::default(),
modeline: None,
_subscriptions: Vec::new(),
encoding: encoding_rs::UTF_8,
has_bom: false,
@ -1175,6 +1178,7 @@ impl Buffer {
text: Rope,
language: Option<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();
@ -1199,6 +1203,7 @@ impl Buffer {
language,
non_text_state_update_count: 0,
capability: Capability::ReadOnly,
modeline,
}
}
}
@ -1225,6 +1230,7 @@ impl Buffer {
language: None,
non_text_state_update_count: 0,
capability: Capability::ReadOnly,
modeline: None,
}
}
@ -1255,6 +1261,7 @@ impl Buffer {
language,
non_text_state_update_count: 0,
capability: Capability::ReadOnly,
modeline: None,
}
}
@ -1285,6 +1292,7 @@ impl Buffer {
language: self.language.clone(),
non_text_state_update_count: self.non_text_state_update_count,
capability: self.capability,
modeline: self.modeline.clone(),
}
}
@ -1537,6 +1545,21 @@ impl Buffer {
);
}
/// Assign the buffer [`ModelineSettings`].
pub fn set_modeline(&mut self, modeline: Option<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 {
@ -2755,8 +2778,12 @@ impl Buffer {
} else {
// The auto-indent setting is not present in editorconfigs, hence
// we can avoid passing the file here.
let auto_indent_mode =
language_settings(language.map(|l| l.name()), None, cx).auto_indent;
let auto_indent_mode = LanguageSettings::resolve(
None,
language.map(|l| l.name()).as_ref(),
cx,
)
.auto_indent;
let apply_syntax_indent = auto_indent_mode == AutoIndentMode::SyntaxAware;
previous_setting = Some((language_id, apply_syntax_indent));
apply_syntax_indent
@ -3397,11 +3424,7 @@ impl BufferSnapshot {
/// Returns [`IndentSize`] for a given position that respects user settings
/// and language preferences.
pub fn language_indent_size_at<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 {
@ -3867,6 +3890,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()
@ -3885,11 +3913,7 @@ impl BufferSnapshot {
position: D,
cx: &'a App,
) -> Cow<'a, LanguageSettings> {
language_settings(
self.language_at(position).map(|l| l.name()),
self.file.as_ref(),
cx,
)
LanguageSettings::for_buffer_snapshot(self, Some(position.to_offset(self)), cx)
}
pub fn char_classifier_at<T: ToOffset>(&self, point: T) -> CharClassifier {
@ -5511,6 +5535,7 @@ impl Clone for BufferSnapshot {
tree_sitter_data: self.tree_sitter_data.clone(),
non_text_state_update_count: self.non_text_state_update_count,
capability: self.capability,
modeline: self.modeline.clone(),
}
}
}

View file

@ -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()
});

View file

@ -12,6 +12,7 @@ mod highlight_map;
mod language_registry;
pub mod language_settings;
mod manifest;
pub mod modeline;
mod outline;
pub mod proto;
mod syntax_map;
@ -40,6 +41,7 @@ use lsp::{
CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServerBinaryOptions, Uri,
};
pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
pub use modeline::{ModelineSettings, parse_modeline};
use parking_lot::Mutex;
use regex::Regex;
use schemars::{JsonSchema, SchemaGenerator, json_schema};
@ -138,6 +140,7 @@ pub static PLAIN_TEXT: LazyLock<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![
@ -1010,6 +1013,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.

View file

@ -745,6 +745,44 @@ impl LanguageRegistry {
.cloned()
}
/// Look up a language by its modeline name (vim filetype or emacs mode).
///
/// This performs a case-insensitive match against:
/// 1. Explicit modeline aliases defined in the language config
/// 2. The language's grammar name
/// 3. The language name itself
pub fn available_language_for_modeline_name(
self: &Arc<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>,

View 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,
@ -17,22 +19,10 @@ pub use settings::{
LanguageSettingsContent, LspInsertMode, RewrapBehavior, ShowWhitespaceSetting, SoftWrap,
WordsCompletionMode,
};
use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore};
use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom};
use shellexpand;
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
/// Returns the settings for the specified language from the provided file.
pub fn language_settings<'a>(
language: Option<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>(
@ -284,6 +274,74 @@ impl LanguageSettings {
/// A token representing the rest of the available language servers.
const REST_OF_LANGUAGE_SERVERS: &'static str = "...";
pub fn for_buffer<'a>(buffer: &'a Buffer, cx: &'a App) -> Cow<'a, LanguageSettings> {
Self::resolve(Some(buffer), None, cx)
}
pub fn for_buffer_at<'a, D: ToOffset>(
buffer: &'a Buffer,
position: D,
cx: &'a App,
) -> Cow<'a, LanguageSettings> {
let language = buffer.language_at(position);
Self::resolve(Some(buffer), language.map(|l| l.name()).as_ref(), cx)
}
pub fn for_buffer_snapshot<'a>(
buffer: &'a BufferSnapshot,
offset: Option<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(
@ -530,6 +588,42 @@ impl AllLanguageSettings {
}
}
fn merge_with_modeline(settings: &mut LanguageSettings, modeline: &ModelineSettings) {
let show_whitespaces = modeline.show_trailing_whitespace.and_then(|v| {
if v {
Some(ShowWhitespaceSetting::Trailing)
} else {
None
}
});
settings
.tab_size
.merge_from_option(modeline.tab_size.as_ref());
settings
.hard_tabs
.merge_from_option(modeline.hard_tabs.as_ref());
settings
.preferred_line_length
.merge_from_option(modeline.preferred_line_length.map(u32::from).as_ref());
let auto_indent_mode = modeline.auto_indent.map(|enabled| {
if enabled {
AutoIndentMode::SyntaxAware
} else {
AutoIndentMode::None
}
});
settings
.auto_indent
.merge_from_option(auto_indent_mode.as_ref());
settings
.show_whitespaces
.merge_from_option(show_whitespaces.as_ref());
settings
.ensure_final_newline_on_save
.merge_from_option(modeline.ensure_final_newline.as_ref());
}
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
let preferred_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
MaxLineLen::Value(u) => Some(u as u32),
@ -557,22 +651,18 @@ fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigPr
TrimTrailingWs::Value(b) => b,
})
.ok();
fn merge<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 {

View file

@ -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));
}
}

View file

@ -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)
}

View file

@ -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 = "}])"

View file

@ -1,6 +1,7 @@
name = "C++"
grammar = "cpp"
path_suffixes = ["cc", "ccm", "hh", "cpp", "cppm", "h", "hpp", "cxx", "cxxm", "hxx", "c++", "c++m", "h++", "ipp", "inl", "ino", "ixx", "cu", "cuh", "C", "H"]
modeline_aliases = ["c++", "cpp", "cxx"]
line_comments = ["// ", "/// ", "//! "]
first_line_pattern = '^//.*-\*-\s*C\+\+\s*-\*-'
decrease_indent_patterns = [

View file

@ -2,12 +2,12 @@ use anyhow::{Context as _, Result};
use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
use gpui::{App, AsyncApp, Task};
use gpui::{App, AsyncApp, Entity, Task};
use http_client::github::latest_github_release;
pub use language::*;
use language::{
LanguageName, LanguageToolchainStore, LspAdapterDelegate, LspInstaller,
language_settings::language_settings,
language_settings::LanguageSettings,
};
use lsp::{LanguageServerBinary, LanguageServerName};
@ -211,7 +211,7 @@ impl LspAdapter for GoLspAdapter {
cx: &mut AsyncApp,
) -> Result<Option<serde_json::Value>> {
let semantic_tokens_enabled = cx.update(|cx| {
language_settings(Some(LanguageName::new("Go")), None, cx)
LanguageSettings::resolve(None, Some(&LanguageName::new("Go")), cx)
.semantic_tokens
.enabled()
});
@ -593,7 +593,7 @@ impl ContextProvider for GoContextProvider {
)))
}
fn associated_tasks(&self, _: Option<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 {

View file

@ -1,6 +1,7 @@
name = "Go"
grammar = "go"
path_suffixes = ["go"]
modeline_aliases = ["golang"]
line_comments = ["// "]
first_line_pattern = '^//.*\bgo run\b'
autoclose_before = ";:.,=}])>"

View file

@ -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 = ["// "]

View file

@ -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());

View file

@ -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()

View file

@ -1,6 +1,7 @@
name = "Markdown"
grammar = "markdown"
path_suffixes = ["md", "mdx", "mdwn", "mdc", "markdown", "MD"]
modeline_aliases = ["md"]
completion_query_characters = ["-"]
block_comment = { start = "<!--", prefix = "", end = "-->", tab_size = 0 }
autoclose_before = ";:.,=}])>"

View file

@ -5,10 +5,12 @@ use collections::HashMap;
use futures::future::BoxFuture;
use futures::lock::OwnedMutexGuard;
use futures::{AsyncBufReadExt, StreamExt as _};
use gpui::{App, AsyncApp, SharedString, Task};
use gpui::{App, AsyncApp, Entity, SharedString, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
use language::language_settings::language_settings;
use language::{ContextLocation, DynLspInstaller, LanguageToolchainStore, LspInstaller, Symbol};
use language::language_settings::LanguageSettings;
use language::{
Buffer, ContextLocation, DynLspInstaller, LanguageToolchainStore, LspInstaller, Symbol,
};
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata};
@ -831,11 +833,10 @@ impl ContextProvider for PythonContextProvider {
toolchains: Arc<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();
@ -873,10 +874,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
@ -983,9 +984,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)

View file

@ -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 = [

View file

@ -3,7 +3,7 @@ use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
use futures::lock::OwnedMutexGuard;
use gpui::{App, AppContext, AsyncApp, SharedString, Task};
use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task};
use http_client::github::AssetKind;
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
use http_client::github_download::{GithubBinaryMetadata, download_server_binary};
@ -32,7 +32,7 @@ use util::rel_path::RelPath;
use util::{ResultExt, maybe};
use crate::LanguageDir;
use crate::language_settings::language_settings;
use crate::language_settings::LanguageSettings;
pub(crate) fn semantic_token_rules() -> SemanticTokenRules {
let content = LanguageDir::get("rust/semantic_token_rules.json")
@ -898,23 +898,16 @@ impl ContextProvider for RustContextProvider {
fn associated_tasks(
&self,
file: Option<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 {

View file

@ -1,6 +1,7 @@
name = "Rust"
grammar = "rust"
path_suffixes = ["rs"]
modeline_aliases = ["rs", "rustic"]
line_comments = ["// ", "/// ", "//! "]
autoclose_before = ";:.,=}])>"
brackets = [

View file

@ -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 }

View file

@ -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 {

View file

@ -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 }

View file

@ -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 = [

View file

@ -23,13 +23,14 @@ use language::{
IndentGuideSettings, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline,
OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _,
ToPoint as _, TransactionId, TreeSitterOptions, Unclipped,
language_settings::{LanguageSettings, language_settings},
language_settings::{AllLanguageSettings, LanguageSettings},
};
#[cfg(any(test, feature = "test-support"))]
use gpui::AppContext as _;
use rope::DimensionPair;
use settings::Settings;
use smallvec::SmallVec;
use smol::future::yield_now;
use std::{
@ -2576,10 +2577,7 @@ impl MultiBuffer {
.map(|excerpt| excerpt.buffer.remote_id());
buffer_id
.and_then(|buffer_id| self.buffer(buffer_id))
.map(|buffer| {
let buffer = buffer.read(cx);
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
})
.map(|buffer| LanguageSettings::for_buffer(&buffer.read(cx), cx))
.unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::default(), cx))
}
@ -2588,14 +2586,11 @@ impl MultiBuffer {
point: T,
cx: &'a App,
) -> Cow<'a, LanguageSettings> {
let mut language = None;
let mut file = None;
if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) {
let buffer = buffer.read(cx);
language = buffer.language_at(offset);
file = buffer.file();
LanguageSettings::for_buffer_at(buffer.read(cx), offset, cx)
} else {
Cow::Borrowed(&AllLanguageSettings::get_global(cx).defaults)
}
language_settings(language.map(|l| l.name()), file, cx)
}
pub fn for_each_buffer(&self, f: &mut dyn FnMut(&Entity<Buffer>)) {
@ -6603,8 +6598,7 @@ impl MultiBufferSnapshot {
let end_row = MultiBufferRow(range.end.row);
let mut row_indents = self.line_indents(start_row, |buffer| {
let settings =
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx);
let settings = LanguageSettings::for_buffer_snapshot(buffer, None, cx);
settings.indent_guides.enabled || ignore_disabled_for_language
});
@ -6628,7 +6622,7 @@ impl MultiBufferSnapshot {
.get_or_insert_with(|| {
(
buffer.remote_id(),
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx),
LanguageSettings::for_buffer_snapshot(buffer, None, cx),
)
})
.1;
@ -6724,13 +6718,7 @@ impl MultiBufferSnapshot {
self.excerpts
.first()
.map(|excerpt| &excerpt.buffer)
.map(|buffer| {
language_settings(
buffer.language().map(|language| language.name()),
buffer.file(),
cx,
)
})
.map(|buffer| LanguageSettings::for_buffer_snapshot(buffer, None, cx))
.unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::ZERO, cx))
}
@ -6739,13 +6727,11 @@ impl MultiBufferSnapshot {
point: T,
cx: &'a App,
) -> Cow<'a, LanguageSettings> {
let mut language = None;
let mut file = None;
if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
language = buffer.language_at(offset);
file = buffer.file();
buffer.settings_at(offset, cx)
} else {
Cow::Borrowed(&AllLanguageSettings::get_global(cx).defaults)
}
language_settings(language.map(|l| l.name()), file, cx)
}
pub fn language_scope_at<T: ToOffset>(&self, point: T) -> Option<LanguageScope> {

View file

@ -23,7 +23,7 @@ use gpui::{
uniform_list,
};
use itertools::Itertools;
use language::language_settings::language_settings;
use language::language_settings::LanguageSettings;
use language::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem};
use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious};
use std::{
@ -855,12 +855,8 @@ impl OutlinePanel {
.read(cx)
.buffer_for_id(*buffer_id, cx)?;
let buffer = buffer.read(cx);
let doc_symbols = language_settings(
buffer.language().map(|l| l.name()),
buffer.file(),
cx,
)
.document_symbols;
let doc_symbols =
LanguageSettings::for_buffer(buffer, cx).document_symbols;
Some((*buffer_id, doc_symbols))
})
.collect();

View file

@ -2,8 +2,8 @@ use anyhow::Context as _;
use collections::{HashMap, HashSet};
use fs::Fs;
use gpui::{AsyncApp, Entity};
use language::language_settings::PrettierSettings;
use language::{Buffer, Diff, Language, language_settings::language_settings};
use language::language_settings::{LanguageSettings, PrettierSettings};
use language::{Buffer, Diff, Language};
use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime;
use paths::default_prettier_dir;
@ -356,7 +356,7 @@ impl Prettier {
let params = buffer
.update(cx, |buffer, cx| {
let buffer_language = buffer.language().map(|language| language.as_ref());
let language_settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx);
let language_settings = LanguageSettings::for_buffer(&buffer, cx);
let prettier_settings = &language_settings.prettier;
anyhow::ensure!(
prettier_settings.allowed,
@ -505,11 +505,7 @@ impl Prettier {
let buffer_language =
buffer.language().map(|language| language.as_ref());
let language_settings = language_settings(
buffer_language.map(|l| l.name()),
buffer.file(),
cx,
);
let language_settings = LanguageSettings::for_buffer(buffer, cx);
let prettier_settings = &language_settings.prettier;
let parser = prettier_parser_name(
buffer_path.as_deref(),

View file

@ -18,7 +18,7 @@ use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::FluentBuilder};
use language::{
Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CharScopeContext,
OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped,
language_settings::{InlayHintKind, LanguageSettings, language_settings},
language_settings::{InlayHintKind, LanguageSettings},
point_from_lsp, point_to_lsp,
proto::{
deserialize_anchor, deserialize_anchor_range, deserialize_version, serialize_anchor,
@ -2936,9 +2936,7 @@ impl LspCommand for OnTypeFormatting {
.await?;
let options = buffer.update(&mut cx, |buffer, cx| {
lsp_formatting_options(
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).as_ref(),
)
lsp_formatting_options(LanguageSettings::for_buffer(buffer, cx).as_ref())
});
Ok(Self {

View file

@ -73,13 +73,12 @@ use language::{
Bias, BinaryStatus, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel,
Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language,
LanguageName, LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, LspInstaller,
ManifestDelegate, ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16,
Toolchain, Transaction, Unclipped,
ManifestDelegate, ManifestName, ModelineSettings, Patch, PointUtf16, TextBufferSnapshot,
ToOffset, ToPointUtf16, Toolchain, Transaction, Unclipped,
language_settings::{
AllLanguageSettings, FormatOnSave, Formatter, LanguageSettings, all_language_settings,
language_settings,
},
point_to_lsp,
modeline, point_to_lsp,
proto::{
deserialize_anchor, deserialize_anchor_range, deserialize_version, serialize_anchor,
serialize_anchor_range, serialize_version,
@ -1601,9 +1600,7 @@ impl LocalLspStore {
.language_servers_for_buffer(buffer, cx)
.map(|(adapter, lsp)| (adapter.clone(), lsp.clone()))
.collect::<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();
let request_timeout = ProjectSettings::get_global(cx)
.global_lsp_settings
.get_request_timeout();
@ -4464,6 +4461,7 @@ impl LspStore {
})
.detach();
self.parse_modeline(buffer, cx);
self.detect_language_for_buffer(buffer, cx);
if let Some(local) = self.as_local_mut() {
local.initialize_buffer(buffer, cx);
@ -4513,6 +4511,16 @@ impl LspStore {
})
}
fn on_buffer_reloaded(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
if self.parse_modeline(&buffer, cx) {
self.detect_language_for_buffer(&buffer, cx);
}
let buffer_id = buffer.read(cx).remote_id();
let task = self.pull_diagnostics_for_buffer(buffer, cx);
self.buffer_reload_tasks.insert(buffer_id, task);
}
pub(crate) fn register_buffer_with_language_servers(
&mut self,
buffer: &Entity<Buffer>,
@ -4736,6 +4744,56 @@ 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.clip_offset(content.len().min(MAX_MODELINE_BYTES), Bias::Left);
let mut first_lines = Vec::new();
let mut lines = content.chunks_in_range(0..first_bytes).lines();
for _ in 0..modeline_lines {
if let Some(line) = lines.next() {
first_lines.push(line.to_string());
} else {
break;
}
}
let first_lines_ref: Vec<_> = first_lines.iter().map(|line| line.as_str()).collect();
let last_start =
content.clip_offset(content.len().saturating_sub(MAX_MODELINE_BYTES), Bias::Left);
let mut last_lines = Vec::new();
let mut lines = content
.reversed_chunks_in_range(last_start..content.len())
.lines();
for _ in 0..modeline_lines {
if let Some(line) = lines.next() {
last_lines.push(line.to_string());
} else {
break;
}
}
let last_lines_ref: Vec<_> =
last_lines.iter().rev().map(|line| line.as_str()).collect();
modeline::parse_modeline(&first_lines_ref, &last_lines_ref)
};
log::debug!("Parsed modeline settings: {:?}", modeline_settings);
buffer_handle.update(cx, |buffer, _cx| buffer.set_modeline(modeline_settings))
}
fn detect_language_for_buffer(
&mut self,
buffer_handle: &Entity<Buffer>,
@ -4744,9 +4802,19 @@ impl LspStore {
// If the buffer has a language, set it and start the language server if we haven't already.
let buffer = buffer_handle.read(cx);
let file = buffer.file()?;
let content = buffer.as_rope();
let available_language = self.languages.language_for_file(file, Some(content), cx);
let modeline_settings = buffer.modeline().map(Arc::as_ref);
let available_language = if let Some(ModelineSettings {
mode: Some(mode_name),
..
}) = modeline_settings
{
self.languages
.available_language_for_modeline_name(mode_name)
} else {
self.languages.language_for_file(file, Some(content), cx)
};
if let Some(available_language) = &available_language {
if let Some(Ok(Ok(new_language))) = self
.languages
@ -4791,8 +4859,12 @@ impl LspStore {
}
});
let settings =
language_settings(Some(new_language.name()), buffer_file.as_ref(), cx).into_owned();
let settings = LanguageSettings::resolve(
Some(&buffer_entity.read(cx)),
Some(&new_language.name()),
cx,
)
.into_owned();
let buffer_file = File::from_dyn(buffer_file.as_ref());
let worktree_id = if let Some(file) = buffer_file {
@ -5100,10 +5172,9 @@ impl LspStore {
let mut language_formatters_to_check = Vec::new();
for buffer in self.buffer_store.read(cx).buffers() {
let buffer = buffer.read(cx);
let buffer_file = File::from_dyn(buffer.file());
let buffer_language = buffer.language();
let settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx);
if buffer_language.is_some() {
let settings = LanguageSettings::for_buffer(buffer, cx);
if buffer.language().is_some() {
let buffer_file = File::from_dyn(buffer.file());
language_formatters_to_check.push((
buffer_file.map(|f| f.worktree_id(cx)),
settings.into_owned(),
@ -5553,9 +5624,9 @@ impl LspStore {
})
.filter(|_| {
maybe!({
let language = buffer.read(cx).language_at(position)?;
buffer.read(cx).language_at(position)?;
Some(
language_settings(Some(language.name()), buffer.read(cx).file(), cx)
LanguageSettings::for_buffer_at(&buffer.read(cx), position, cx)
.linked_edits,
)
}) == Some(true)
@ -5659,12 +5730,7 @@ impl LspStore {
) -> Task<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(),
)
});
@ -6206,13 +6272,9 @@ impl LspStore {
let offset = position.to_offset(&snapshot);
let scope = snapshot.language_scope_at(offset);
let language = snapshot.language().cloned();
let completion_settings = language_settings(
language.as_ref().map(|language| language.name()),
buffer.read(cx).file(),
cx,
)
.completions
.clone();
let completion_settings = LanguageSettings::for_buffer(&buffer.read(cx), cx)
.completions
.clone();
if !completion_settings.lsp {
return Task::ready(Ok(Vec::new()));
}
@ -7966,12 +8028,6 @@ impl LspStore {
None
}
fn on_buffer_reloaded(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
let buffer_id = buffer.read(cx).remote_id();
let task = self.pull_diagnostics_for_buffer(buffer, cx);
self.buffer_reload_tasks.insert(buffer_id, task);
}
async fn refresh_workspace_configurations(lsp_store: &WeakEntity<Self>, cx: &mut AsyncApp) {
maybe!(async move {
let mut refreshed_servers = HashSet::default();

View file

@ -15,7 +15,7 @@ use gpui::{App, AppContext as _, Context, Entity, SharedString, Task, WeakEntity
use itertools::Itertools;
use language::{
Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location,
language_settings::language_settings,
language_settings::LanguageSettings,
};
use lsp::{LanguageServerId, LanguageServerName};
use paths::{debug_task_file_name, task_file_name};
@ -312,17 +312,15 @@ impl Inventory {
let last_scheduled_scenarios = self.last_scheduled_scenarios.iter().cloned().collect();
let adapter = task_contexts.location().and_then(|location| {
let (file, language) = {
let buffer = location.buffer.read(cx);
(buffer.file(), buffer.language())
};
let language_name = language.as_ref().map(|l| l.name());
let adapter = language_settings(language_name, file, cx)
let buffer = location.buffer.read(cx);
let adapter = LanguageSettings::for_buffer(&buffer, cx)
.debuggers
.first()
.map(SharedString::from)
.or_else(|| {
language.and_then(|l| l.config().debuggers.first().map(SharedString::from))
buffer
.language()
.and_then(|l| l.config().debuggers.first().map(SharedString::from))
});
adapter.map(|adapter| (adapter, DapRegistry::global(cx).locators()))
});
@ -360,19 +358,18 @@ impl Inventory {
label: &str,
cx: &App,
) -> Task<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
@ -388,7 +385,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,
@ -404,14 +401,18 @@ impl Inventory {
});
let language_tasks = language
.filter(|language| {
language_settings(Some(language.name()), file.as_ref(), cx)
.tasks
.enabled
LanguageSettings::resolve(
buffer.as_ref().map(|b| b.read(cx)),
Some(&language.name()),
cx,
)
.tasks
.enabled
})
.and_then(|language| {
language
.context_provider()
.map(|provider| provider.associated_tasks(file, cx))
.map(|provider| provider.associated_tasks(buffer, cx))
});
cx.background_spawn(async move {
if let Some(t) = language_tasks {
@ -445,7 +446,7 @@ impl Inventory {
let task_source_kind = language.as_ref().map(|language| TaskSourceKind::Language {
name: language.name().into(),
});
let file = location.and_then(|location| location.buffer.read(cx).file().cloned());
let buffer = location.map(|location| location.buffer.clone());
let worktrees_with_zed_tasks: HashSet<WorktreeId> = self
.templates_from_settings
@ -507,14 +508,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()
@ -1036,7 +1041,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()))
}
}

View file

@ -36,7 +36,7 @@ use git::{
use git2::RepositoryInitOptions;
use gpui::{
App, AppContext, BackgroundExecutor, BorrowAppContext, Entity, FutureExt, SharedString, Task,
UpdateGlobal,
TestAppContext, UpdateGlobal,
};
use itertools::Itertools;
use language::{
@ -44,7 +44,7 @@ use language::{
DiagnosticSourceKind, DiskState, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
LanguageName, LineEnding, ManifestName, ManifestProvider, ManifestQuery, OffsetRangeExt, Point,
ToPoint, Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata,
language_settings::{LanguageSettingsContent, language_settings},
language_settings::{LanguageSettings, LanguageSettingsContent},
markdown_lang, rust_lang, tree_sitter_typescript,
};
use lsp::{
@ -296,50 +296,43 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
cx.executor().run_until_parked();
cx.update(|cx| {
let tree = worktree.read(cx);
let settings_for = |path: &str| {
let file_entry = tree.entry_for_path(rel_path(path)).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
language_settings(Some(file_language.name()), Some(&file), cx).into_owned()
};
let settings_for = async |path: &str, cx: &mut TestAppContext| -> LanguageSettings {
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), rel_path(path)), cx)
})
.await
.unwrap();
cx.update(|cx| LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned())
};
let settings_a = settings_for("a.rs");
let settings_b = settings_for("b/b.rs");
let settings_c = settings_for("c.js");
let settings_d = settings_for("d/d.rs");
let settings_readme = settings_for("README.json");
let settings_a = settings_for("a.rs", cx).await;
let settings_b = settings_for("b/b.rs", cx).await;
let settings_c = settings_for("c.js", cx).await;
let settings_d = settings_for("d/d.rs", cx).await;
let settings_readme = settings_for("README.json", cx).await;
// .editorconfig overrides .zed/settings
assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3));
assert_eq!(settings_a.hard_tabs, true);
assert_eq!(settings_a.ensure_final_newline_on_save, true);
assert_eq!(settings_a.remove_trailing_whitespace_on_save, true);
assert_eq!(settings_a.preferred_line_length, 120);
// .editorconfig overrides .zed/settings
assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3));
assert_eq!(settings_a.hard_tabs, true);
assert_eq!(settings_a.ensure_final_newline_on_save, true);
assert_eq!(settings_a.remove_trailing_whitespace_on_save, true);
assert_eq!(settings_a.preferred_line_length, 120);
// .editorconfig in b/ overrides .editorconfig in root
assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2));
// .editorconfig in subdirectory overrides .editorconfig in root
assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2));
assert_eq!(Some(settings_d.tab_size), NonZeroU32::new(1));
// .editorconfig in subdirectory overrides .editorconfig in root
assert_eq!(Some(settings_d.tab_size), NonZeroU32::new(1));
// "indent_size" is not set, so "tab_width" is used
assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10));
// "indent_size" is not set, so "tab_width" is used
assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10));
// When max_line_length is "off", default to .zed/settings.json
assert_eq!(settings_b.preferred_line_length, 64);
assert_eq!(settings_c.preferred_line_length, 64);
// When max_line_length is "off", default to .zed/settings.json
assert_eq!(settings_b.preferred_line_length, 64);
assert_eq!(settings_c.preferred_line_length, 64);
// README.md should not be affected by .editorconfig's globe "*.rs"
assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8));
});
// README.md should not be affected by .editorconfig's globe "*.rs"
assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8));
}
#[gpui::test]
@ -373,37 +366,28 @@ async fn test_external_editorconfig_support(cx: &mut gpui::TestAppContext) {
let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
cx.executor().run_until_parked();
let settings_for = async |path: &str, cx: &mut TestAppContext| -> LanguageSettings {
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), rel_path(path)), cx)
})
.await
.unwrap();
cx.update(|cx| LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned())
};
cx.update(|cx| {
let tree = worktree.read(cx);
let settings_for = |path: &str| {
let file_entry = tree.entry_for_path(rel_path(path)).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
language_settings(Some(file_language.name()), Some(&file), cx).into_owned()
};
let settings_rs = settings_for("main.rs", cx).await;
let settings_md = settings_for("README.md", cx).await;
let settings_txt = settings_for("other.txt", cx).await;
let settings_rs = settings_for("main.rs");
let settings_md = settings_for("README.md");
let settings_txt = settings_for("other.txt");
// main.rs gets indent_size = 2 from parent's external .editorconfig
assert_eq!(Some(settings_rs.tab_size), NonZeroU32::new(2));
// main.rs gets indent_size = 2 from parent's external .editorconfig
assert_eq!(Some(settings_rs.tab_size), NonZeroU32::new(2));
// README.md gets indent_size = 3 from internal worktree .editorconfig
assert_eq!(Some(settings_md.tab_size), NonZeroU32::new(3));
// README.md gets indent_size = 3 from internal worktree .editorconfig
assert_eq!(Some(settings_md.tab_size), NonZeroU32::new(3));
// other.txt gets indent_size = 4 from grandparent's external .editorconfig
assert_eq!(Some(settings_txt.tab_size), NonZeroU32::new(4));
});
// other.txt gets indent_size = 4 from grandparent's external .editorconfig
assert_eq!(Some(settings_txt.tab_size), NonZeroU32::new(4));
}
#[gpui::test]
@ -432,24 +416,14 @@ async fn test_internal_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppC
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), rel_path("src/file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let tree = worktree.read(cx);
let file_entry = tree
.entry_for_path(rel_path("src/file.rs"))
.unwrap()
.clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
let settings = LanguageSettings::for_buffer(buffer.read(cx), cx).into_owned();
assert_eq!(Some(settings.tab_size), NonZeroU32::new(2));
});
}
@ -480,20 +454,15 @@ async fn test_external_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppC
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let tree = worktree.read(cx);
let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx);
// file.rs gets indent_size = 2 from worktree's root config, NOT 99 from parent
assert_eq!(Some(settings.tab_size), NonZeroU32::new(2));
@ -528,20 +497,15 @@ async fn test_external_editorconfig_root_in_parent_stops_traversal(cx: &mut gpui
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let tree = worktree.read(cx);
let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx);
// file.rs gets indent_size = 4 from parent's root config, NOT 99 from grandparent
assert_eq!(Some(settings.tab_size), NonZeroU32::new(4));
@ -584,30 +548,24 @@ async fn test_external_editorconfig_shared_across_worktrees(cx: &mut gpui::TestA
cx.executor().run_until_parked();
cx.update(|cx| {
let worktrees: Vec<_> = project.read(cx).worktrees(cx).collect();
assert_eq!(worktrees.len(), 2);
let worktrees: Vec<_> = cx.update(|cx| project.read(cx).worktrees(cx).collect());
assert_eq!(worktrees.len(), 2);
for worktree in worktrees {
let tree = worktree.read(cx);
let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings =
language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
for worktree in worktrees {
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx);
// Both worktrees should get indent_size = 5 from shared parent .editorconfig
assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
}
});
});
}
}
#[gpui::test]
@ -637,20 +595,15 @@ async fn test_external_editorconfig_not_loaded_without_internal_config(
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let tree = worktree.read(cx);
let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx);
// file.rs should have default tab_size = 4, NOT 99 from parent's external .editorconfig
// because without an internal .editorconfig, external configs are not loaded
@ -684,20 +637,15 @@ async fn test_external_editorconfig_modification_triggers_refresh(cx: &mut gpui:
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let tree = worktree.read(cx);
let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx);
// Test initial settings: tab_size = 4 from parent's external .editorconfig
assert_eq!(Some(settings.tab_size), NonZeroU32::new(4));
@ -712,20 +660,15 @@ async fn test_external_editorconfig_modification_triggers_refresh(cx: &mut gpui:
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree.read(cx).id(), rel_path("file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let tree = worktree.read(cx);
let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx);
// Test settings updated: tab_size = 8
assert_eq!(Some(settings.tab_size), NonZeroU32::new(8));
@ -760,21 +703,16 @@ async fn test_adding_worktree_discovers_external_editorconfigs(cx: &mut gpui::Te
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
let id = project.worktrees(cx).next().unwrap().read(cx).id();
project.open_buffer((id, rel_path("file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let worktree = project.read(cx).worktrees(cx).next().unwrap();
let tree = worktree.read(cx);
let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned();
// Test existing worktree has tab_size = 7
assert_eq!(Some(settings.tab_size), NonZeroU32::new(7));
@ -789,20 +727,15 @@ async fn test_adding_worktree_discovers_external_editorconfigs(cx: &mut gpui::Te
cx.executor().run_until_parked();
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((new_worktree.read(cx).id(), rel_path("file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let tree = new_worktree.read(cx);
let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
let file = File::for_entry(file_entry, new_worktree.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx);
// Verify new worktree also has tab_size = 7 from shared parent editorconfig
assert_eq!(Some(settings.tab_size), NonZeroU32::new(7));
@ -943,20 +876,15 @@ async fn test_shared_external_editorconfig_cleanup_with_multiple_worktrees(
assert_eq!(watcher_paths.len(), 1);
});
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_b.read(cx).id(), rel_path("file.rs")), cx)
})
.await
.unwrap();
cx.update(|cx| {
let tree = worktree_b.read(cx);
let file_entry = tree.entry_for_path(rel_path("file.rs")).unwrap().clone();
let file = File::for_entry(file_entry, worktree_b.clone());
let file_language = project
.read(cx)
.languages()
.load_language_for_file_path(file.path.as_std_path());
let file_language = cx
.foreground_executor()
.block_on(file_language)
.expect("Failed to get file language");
let file = file as _;
let settings = language_settings(Some(file_language.name()), Some(&file), cx).into_owned();
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx);
// Test worktree_b still has correct settings
assert_eq!(Some(settings.tab_size), NonZeroU32::new(5));
@ -1083,27 +1011,29 @@ 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);
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 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);
});
let all_tasks = cx
.update(|cx| get_all_tasks(&project, task_contexts.clone(), cx))
.await
.into_iter()
.map(|(source_kind, task)| {
let resolved = task.resolved;

View file

@ -148,7 +148,7 @@ async fn test_multiline_regex(cx: &mut gpui::TestAppContext) {
use language::Buffer;
let text = Rope::from("hello\nworld\nhello\nworld");
let snapshot = cx
.update(|app| Buffer::build_snapshot(text, None, None, app))
.update(|app| Buffer::build_snapshot(text, None, None, None, app))
.await;
let results = search_query.search(&snapshot, None).await;

View file

@ -7,6 +7,7 @@ use client::{Client, UserStore};
use clock::FakeSystemClock;
use collections::{HashMap, HashSet};
use language_model::LanguageModelToolResultContent;
use languages::rust_lang;
use extension::ExtensionHostProxy;
use fs::{FakeFs, Fs};
@ -14,7 +15,7 @@ use gpui::{AppContext as _, Entity, SharedString, TestAppContext};
use http_client::{BlockedHttpClient, FakeHttpClient};
use language::{
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding,
language_settings::{AllLanguageSettings, language_settings},
language_settings::{AllLanguageSettings, LanguageSettings},
};
use lsp::{
CompletionContext, CompletionResponse, CompletionTriggerKind, DEFAULT_LSP_REQUEST_TIMEOUT,
@ -481,6 +482,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
let worktree_id = project
.update(cx, |project, cx| {
project.languages().add(rust_lang());
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
@ -521,9 +523,8 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
});
cx.read(|cx| {
let file = buffer.read(cx).file();
assert_eq!(
language_settings(Some("Rust".into()), file, cx).language_servers,
LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
["override-rust-analyzer".to_string()]
)
});
@ -646,6 +647,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
let worktree_id = project
.update(cx, |project, cx| {
project.languages().add(rust_lang());
project.find_or_create_worktree(path!("/code/project1"), true, cx)
})
.await
@ -668,9 +670,8 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
let fake_second_lsp = fake_second_lsp.next().await.unwrap();
cx.read(|cx| {
let file = buffer.read(cx).file();
assert_eq!(
language_settings(Some("Rust".into()), file, cx).language_servers,
LanguageSettings::for_buffer(buffer.read(cx), cx).language_servers,
["rust-analyzer".to_string(), "fake-analyzer".to_string()]
)
});

View file

@ -219,6 +219,7 @@ impl VsCodeSettings {
vim_mode: None,
workspace: self.workspace_settings_content(),
which_key: None,
modeline_lines: None,
}
}

View file

@ -204,6 +204,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 {

View file

@ -8543,7 +8543,7 @@ fn language_settings_data() -> Box<[SettingsPageItem]> {
]
}
fn miscellaneous_section() -> [SettingsPageItem; 6] {
fn miscellaneous_section() -> [SettingsPageItem; 7] {
[
SettingsPageItem::SectionHeader("Miscellaneous"),
SettingsPageItem::SettingItem(SettingItem {
@ -8642,6 +8642,19 @@ fn language_settings_data() -> Box<[SettingsPageItem]> {
metadata: None,
files: USER | PROJECT,
}),
SettingsPageItem::SettingItem(SettingItem {
title: "Vim/Emacs Modeline Support",
description: "Number of lines to search for modelines (set to 0 to disable).",
field: Box::new(SettingField {
json_path: Some("modeline_lines"),
pick: |settings_content| settings_content.modeline_lines.as_ref(),
write: |settings_content, value| {
settings_content.modeline_lines = value;
},
}),
metadata: None,
files: USER | PROJECT,
}),
]
}

View file

@ -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;

View file

@ -45,6 +45,7 @@
- [Debugger](./debugger.md)
- [REPL](./repl.md)
- [Git](./git.md)
- [Modelines](./modelines.md)
# Collaboration

View file

@ -69,6 +69,10 @@ Settings are applied in layers:
Later layers override earlier ones. For object settings (like `terminal`), properties merge rather than replace entirely.
## Per-file Settings
Zed has some compatibility support for Emacs and Vim [modelines](./modelines.md), so you can set some settings per-file.
## Per-Release Channel Overrides
Use different settings for Stable, Preview, or Nightly builds by adding top-level channel keys:

67
docs/src/modelines.md Normal file
View file

@ -0,0 +1,67 @@
# Modelines
Modelines are special comments at the beginning or end of a file that configure editor settings for that specific file. Zed supports both Vim and Emacs modeline formats, allowing you to specify settings like tab size, indentation style, and file type directly within your files.
## Configuration
Use the [`modeline_lines`](./reference/all-settings.md#modeline-lines) setting to control how many lines Zed searches for modelines:
```json [settings]
{
"modeline_lines": 5
}
```
Set to `0` to disable modeline parsing entirely.
## Emacs
Zed has some compatibility support for [Emacs file variables](https://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html).
Example:
```python
# -*- mode: python; tab-width: 4; indent-tabs-mode: nil; -*-
```
### Supported Emacs Variables
| Variable | Description | Zed Setting |
| -------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
| `mode` | Major mode/language | Language detection |
| `tab-width` | Tab display width | [`tab_size`](./reference/all-settings.md#tab-size) |
| `fill-column` | Line wrap column | [`preferred_line_length`](./reference/all-settings.md#preferred-line-length) |
| `indent-tabs-mode` | `nil` for spaces, `t` for tabs | [`hard_tabs`](./reference/all-settings.md#hard-tabs) |
| `electric-indent-mode` | Auto-indentation | [`auto_indent`](./reference/all-settings.md#auto-indent) |
| `require-final-newline` | Ensure final newline | [`ensure_final_newline_on_save`](./reference/all-settings.md#ensure-final-newline-on-save) |
| `show-trailing-whitespace` | Show trailing whitespace | [`show_whitespaces`](./reference/all-settings.md#show-whitespaces) |
## Vim
Zed has some compatibility support for [Vim modeline](https://vimhelp.org/options.txt.html#modeline).
Example:
```python
# vim: set ft=python ts=4 sw=4 et:
```
### Supported Vim Options
| Option | Aliases | Description | Zed Setting |
| -------------- | ------- | --------------------------------- | ------------------------------------------------------------------------------------------ |
| `filetype` | `ft` | File type/language | Language detection |
| `tabstop` | `ts` | Number of spaces a tab counts for | [`tab_size`](./reference/all-settings.md#tab-size) |
| `textwidth` | `tw` | Maximum line width | [`preferred_line_length`](./reference/all-settings.md#preferred-line-length) |
| `expandtab` | `et` | Use spaces instead of tabs | [`hard_tabs`](./reference/all-settings.md#hard-tabs) |
| `noexpandtab` | `noet` | Use tabs instead of spaces | [`hard_tabs`](./reference/all-settings.md#hard-tabs) |
| `autoindent` | `ai` | Enable auto-indentation | [`auto_indent`](./reference/all-settings.md#auto-indent) |
| `noautoindent` | `noai` | Disable auto-indentation | [`auto_indent`](./reference/all-settings.md#auto-indent) |
| `endofline` | `eol` | Ensure final newline | [`ensure_final_newline_on_save`](./reference/all-settings.md#ensure-final-newline-on-save) |
| `noendofline` | `noeol` | Disable final newline | [`ensure_final_newline_on_save`](./reference/all-settings.md#ensure-final-newline-on-save) |
## Notes
- The first kilobyte of a file is searched for modelines.
- Emacs modelines take precedence over Vim modelines when both are present.
- Modelines in the first few lines take precedence over those at the end of the file.

View file

@ -93,6 +93,8 @@ extend-ignore-re = [
"ags",
# AMD GPU Services
"AGS",
# "noet" is a vim variable (ideally to ignore locally)
"noet",
# Yarn Plug'n'Play
"PnP"
]