mirror of
https://github.com/zed-industries/zed.git
synced 2026-10-21 05:31:11 +00:00
Stop re-resolving editorconfig sections on every language settings lookup (#63323)
Closes https://github.com/zed-industries/zed/issues/59689
Symbolicating the reporter's profile (21s of typing, Windows) showed
`Window::draw` taking 63% of the main thread at ~62 redraws/s (p95 draw
= 40ms), and 30.9% of the *entire* main thread inside
`EditorconfigStore::properties()`:
every `LanguageSettings` lookup — several per frame via
`DisplayMap::tab_size`, `MultiBuffer::language_settings`, indent guides,
and `LspStore::supports_range_formatting` — re-ran the full editorconfig
resolution (ancestor walk plus `ec4rs::Section::apply_to` over every key
of every section) and then cloned the whole `LanguageSettings` struct.
Real-world `.editorconfig` files carry hundreds of irrelevant keys
(`dotnet_*`, `ij_*`, ...), so the per-call map assembly dominated:
`RtlAllocateHeap` 7.3%, `memcmp` 7.0%, `memcpy` 6.6%,
`Properties::find_idx` 5.9% self time.
## Commit 1: resolve only the editorconfig properties Zed reads
- `EditorconfigStore::properties()` now applies only the 7 keys Zed
consumes (`indent_style`, `indent_size`, `tab_width`, `end_of_line`,
`max_line_length`, `insert_final_newline`, `trim_trailing_whitespace`)
instead of materializing every key of every matching section.
- Worktrees with no editorconfig at all now return `None` early instead
of allocating an empty `Properties` and forcing a `LanguageSettings`
clone on every lookup.
- Adds an `editor_render_with_editorconfig` benchmark: maximized window,
file-backed buffer at `src/app/components/editor_pane.ts`, a root
`.editorconfig` shaped after [the second
report](https://github.com/zed-industries/zed/issues/59689#issuecomment-5475124891)
(no `root = true`, a `[*]` section with ~3200 keys, 25 sections with
large `{...}` alternation lists) plus a 4-level nested chain, and a
typing workload (insert/delete + cursor moves).
```
main (770aac345c): time: [37.905 ms 38.005 ms 38.107 ms]
time: [38.069 ms 38.157 ms 38.248 ms]
this commit: time: [2.8547 ms 2.8618 ms 2.8702 ms]
time: [2.8372 ms 2.8435 ms 2.8510 ms]
```
The candidate is ~13x faster.
GPUI frame report over the same workload: window draw p50 12.71 ms →
0.76 ms, p99 23.38 ms → 1.22 ms; frame budget overruns 556 → 0.
## Commit 2: cache resolved `LanguageSettings` per buffer
- `Buffer` and `BufferSnapshot` now hold `resolved_settings:
Arc<LanguageSettings>`, recomputed by a `SettingsStore` global observer.
- Global methods for retrieving the settings are still there, but buffer
only uses them for multi buffer's `language_settings_at` with an
injected language different from the buffer's primary; cold scopes such
as extension API, LSP workspace configuration, server startup still use
the global resolution
Same benchmark, same method, on top of commit 1 (alternated runs, quiet
machine):
```
commit 1: time: [2.8547 ms 2.8618 ms 2.8702 ms]
time: [2.8372 ms 2.8435 ms 2.8510 ms]
this commit: time: [1.6256 ms 1.6325 ms 1.6403 ms]
change: [-42.685% -42.270% -41.854%] (p = 0.00 < 0.05)
time: [1.6221 ms 1.6284 ms 1.6352 ms]
```
Window draw p50 0.76 ms → 0.47 ms, p99 1.22 ms → 0.69 ms, max 1.02 ms;
zero frame-budget overruns on both sides.
Combined, the benchmark's per-keystroke cost drops ~23x vs `main`
(37.9–38.2 ms → 1.62–1.64 ms).
Release Notes:
- Improved typing and rendering performance in projects that use
`.editorconfig` files.
This commit is contained in:
parent
e6690f38cc
commit
24254879b7
19 changed files with 671 additions and 148 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -2135,6 +2135,7 @@ dependencies = [
|
|||
"futures 0.3.32",
|
||||
"gpui",
|
||||
"gpui_platform",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
"language",
|
||||
"language_model",
|
||||
|
|
@ -2144,6 +2145,7 @@ dependencies = [
|
|||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.9.4",
|
||||
"rpc",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"text",
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ impl PendingDiff {
|
|||
self.new_buffer.read(cx).line_ending(),
|
||||
self.new_buffer.read(cx).as_rope().clone(),
|
||||
);
|
||||
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite);
|
||||
let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite, cx);
|
||||
buffer.set_language(language, cx);
|
||||
buffer
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ editor = { workspace = true, features = ["test-support"] }
|
|||
futures.workspace = true
|
||||
gpui = { workspace = true, features = ["bench-support"] }
|
||||
gpui_platform = { workspace = true, features = ["bench-support", "font-kit"] }
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
|
|
@ -33,6 +34,7 @@ markdown.workspace = true
|
|||
multi_buffer.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
prompt_store.workspace = true
|
||||
rpc.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
text.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,13 +1,59 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use editor::{
|
||||
Editor, EditorMode, MultiBuffer,
|
||||
actions::{DeleteToPreviousWordStart, SelectAll, SplitSelectionIntoLines},
|
||||
};
|
||||
use gpui::{AppContext as _, BenchAppContext, Focusable as _};
|
||||
use gpui::{App, AppContext as _, BenchAppContext, BorrowAppContext as _, Focusable as _};
|
||||
use indoc::{formatdoc, indoc};
|
||||
use language::{Buffer, Capability, DiskState, File, LocalFile};
|
||||
use rand::{Rng as _, SeedableRng as _, rngs::StdRng};
|
||||
use settings::SettingsStore;
|
||||
use util::RandomCharIter;
|
||||
use settings::{LocalSettingsKind, LocalSettingsPath, SettingsStore, WorktreeId};
|
||||
use util::{RandomCharIter, paths::PathStyle, rel_path::RelPath};
|
||||
use zed_actions::editor::{MoveDown, MoveUp};
|
||||
|
||||
struct BenchFile {
|
||||
path: Arc<RelPath>,
|
||||
}
|
||||
|
||||
impl File for BenchFile {
|
||||
fn as_local(&self) -> Option<&dyn LocalFile> {
|
||||
None
|
||||
}
|
||||
|
||||
fn disk_state(&self) -> DiskState {
|
||||
DiskState::New
|
||||
}
|
||||
|
||||
fn path(&self) -> &Arc<RelPath> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn full_path(&self, _: &App) -> PathBuf {
|
||||
PathBuf::from("root").join(self.path.as_std_path())
|
||||
}
|
||||
|
||||
fn path_style(&self, _: &App) -> PathStyle {
|
||||
PathStyle::local()
|
||||
}
|
||||
|
||||
fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
|
||||
self.path.file_name().unwrap_or("root")
|
||||
}
|
||||
|
||||
fn worktree_id(&self, _: &App) -> WorktreeId {
|
||||
WorktreeId::from_usize(0)
|
||||
}
|
||||
|
||||
fn to_proto(&self, _: &App) -> rpc::proto::File {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn is_private(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::bench(
|
||||
inputs = multi_cursor_line_counts(),
|
||||
group = "Multi-cursor input",
|
||||
|
|
@ -124,6 +170,188 @@ fn editor_render(cx: &mut BenchAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::bench]
|
||||
fn editor_render_with_editorconfig(cx: &mut BenchAppContext) {
|
||||
init_context(cx);
|
||||
|
||||
let worktree_id = WorktreeId::from_usize(0);
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||
let nested_configs = [
|
||||
("", jetbrains_editorconfig()),
|
||||
(
|
||||
"src",
|
||||
indoc! {"
|
||||
[*.{ts,tsx}]
|
||||
indent_size = 2
|
||||
"}
|
||||
.to_string(),
|
||||
),
|
||||
(
|
||||
"src/app",
|
||||
indoc! {"
|
||||
[*]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.ts]
|
||||
max_line_length = 100
|
||||
"}
|
||||
.to_string(),
|
||||
),
|
||||
(
|
||||
"src/app/components",
|
||||
indoc! {"
|
||||
[*.{ts,tsx}]
|
||||
indent_style = space
|
||||
"}
|
||||
.to_string(),
|
||||
),
|
||||
];
|
||||
for (directory, content) in nested_configs {
|
||||
store
|
||||
.set_local_settings(
|
||||
worktree_id,
|
||||
LocalSettingsPath::InWorktree(Arc::from(
|
||||
RelPath::from_unix_str(directory).unwrap(),
|
||||
)),
|
||||
LocalSettingsKind::Editorconfig,
|
||||
Some(&content),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
let buffer = cx.update(|cx| {
|
||||
let text = indented_code_text(3000);
|
||||
let file: Arc<dyn File> = Arc::new(BenchFile {
|
||||
path: RelPath::from_unix_str("src/app/components/editor_pane.ts")
|
||||
.unwrap()
|
||||
.into(),
|
||||
});
|
||||
let buffer = cx.new(|cx| {
|
||||
Buffer::build(
|
||||
text::Buffer::new(
|
||||
text::ReplicaId::LOCAL,
|
||||
cx.entity_id().as_non_zero_u64().into(),
|
||||
text,
|
||||
),
|
||||
Some(file),
|
||||
Capability::ReadWrite,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer, cx))
|
||||
});
|
||||
|
||||
let mut window = cx.add_empty_window();
|
||||
let editor = window.update(|window, cx| {
|
||||
let editor = window.replace_root(cx, |window, cx| {
|
||||
let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx);
|
||||
editor.set_style(editor::EditorStyle::default(), window, cx);
|
||||
editor
|
||||
});
|
||||
window.focus(&editor.focus_handle(cx), cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let mut insert = true;
|
||||
cx.bench_renderer(editor, move |editor, window, cx| {
|
||||
if insert {
|
||||
editor.handle_input("x", window, cx);
|
||||
} else {
|
||||
editor.backspace(&editor::actions::Backspace, window, cx);
|
||||
}
|
||||
insert = !insert;
|
||||
editor.move_down(&MoveDown, window, cx);
|
||||
editor.move_up(&MoveUp, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn indented_code_text(line_count: usize) -> String {
|
||||
let mut text = String::new();
|
||||
for block in 0..line_count / 10 {
|
||||
text.push_str(&format!("export function component{block:04}() {{\n"));
|
||||
text.push_str(" const state = {\n");
|
||||
text.push_str(" items: [],\n");
|
||||
text.push_str(" selection: null,\n");
|
||||
text.push_str(" };\n");
|
||||
text.push_str(" if (state.items.length > 0) {\n");
|
||||
text.push_str(" for (const item of state.items) {\n");
|
||||
text.push_str(" console.log(item, state.selection);\n");
|
||||
text.push_str(" }\n");
|
||||
text.push_str(" }\n");
|
||||
text.push_str("}\n");
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn jetbrains_editorconfig() -> String {
|
||||
let mut content = indoc! {"
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
max_line_length = 150
|
||||
tab_width = 4
|
||||
trim_trailing_whitespace = false
|
||||
"}
|
||||
.to_string();
|
||||
for key_index in 0..750 {
|
||||
content.push_str(&format!("ij_continuation_option_{key_index:04} = false\n"));
|
||||
}
|
||||
for key_index in 0..1500 {
|
||||
content.push_str(&format!(
|
||||
"dotnet_diagnostic.ca{key_index:04}.severity = warning\n"
|
||||
));
|
||||
}
|
||||
for key_index in 0..750 {
|
||||
content.push_str(&format!("resharper_style_option_{key_index:04} = true\n"));
|
||||
}
|
||||
let sections = [
|
||||
"*.css",
|
||||
"*.feature",
|
||||
"*.less",
|
||||
"*.properties",
|
||||
"*.proto",
|
||||
"*.sass",
|
||||
"*.scss",
|
||||
"*.vue",
|
||||
".editorconfig",
|
||||
"{*.ant,*.appxmanifest,*.axml,*.cscfg,*.csdef,*.disco,*.filelayout,*.fxml,*.jhm,*.jnlp,*.jrxml,*.manifest,*.myapp,*.nuspec,*.rng,*.stylecop,*.svcmap,*.tld,*.tps,*.wadcfgx,*.webref,*.wsdl,*.xml,*.xsd,*.xsl,*.xslt,*.xul,StyleCop.Cache}",
|
||||
"{*.ats,*.ts}",
|
||||
"{*.bash,*.sh,*.zsh}",
|
||||
"{*.cjs,*.js}",
|
||||
"{*.cjsx,*.coffee}",
|
||||
"{*.har,*.inputactions,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,jest.config}",
|
||||
"{*.hcl,*.nomad}",
|
||||
"{*.htm,*.html,*.ng,*.sht,*.shtm,*.shtml}",
|
||||
"{*.markdown,*.md}",
|
||||
"{*.pb,*.textproto}",
|
||||
"{*.ps1,*.psd1,*.psm1}",
|
||||
"{*.tf,*.tfvars}",
|
||||
"{*.yaml,*.yml}",
|
||||
"*.js.map",
|
||||
"*.{appxmanifest,asax,ascx,aspx,axaml,build,cg,cginc,compute,cs,cshtml,dtd,fs,fsi,fsscript,fsx,hlsl,hlsli,hlslinc,master,ml,mli,nuspec,paml,razor,resw,resx,shader,skin,usf,ush,vb,xaml,xamlx,xoml,xsd}",
|
||||
];
|
||||
for (section_index, section) in sections.iter().enumerate() {
|
||||
content.push_str(&formatdoc! {"
|
||||
|
||||
[{section}]
|
||||
indent_size = 2
|
||||
"});
|
||||
for key_index in 0..40 {
|
||||
content.push_str(&format!(
|
||||
"ij_section_{section_index:02}_option_{key_index:02} = false\n"
|
||||
));
|
||||
}
|
||||
}
|
||||
content
|
||||
}
|
||||
|
||||
fn init_context(cx: &mut BenchAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
|
|
@ -146,6 +374,7 @@ gpui::bench_group!(
|
|||
benches,
|
||||
editor_multi_cursor_input,
|
||||
open_editor_with_one_long_line,
|
||||
editor_render
|
||||
editor_render,
|
||||
editor_render_with_editorconfig
|
||||
);
|
||||
gpui::bench_main!(benches);
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ impl ChannelBuffer {
|
|||
ReplicaId::new(response.replica_id as u16),
|
||||
capability,
|
||||
base_text,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx));
|
||||
|
|
|
|||
|
|
@ -767,8 +767,8 @@ fn make_collaborator_replica(
|
|||
) -> (Entity<Buffer>, clock::Global) {
|
||||
let (state, version) =
|
||||
buffer.read_with(cx, |buffer, _cx| (buffer.to_proto(_cx), buffer.version()));
|
||||
let collaborator = cx.new(|_cx| {
|
||||
Buffer::from_proto(ReplicaId::new(1), Capability::ReadWrite, state, None).unwrap()
|
||||
let collaborator = cx.new(|cx| {
|
||||
Buffer::from_proto(ReplicaId::new(1), Capability::ReadWrite, state, None, cx).unwrap()
|
||||
});
|
||||
(collaborator, version)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1184,7 +1184,7 @@ pub struct Editor {
|
|||
on_local_selections_changed:
|
||||
Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
|
||||
suppress_selection_callback: bool,
|
||||
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
|
||||
applicable_language_settings: HashMap<Option<LanguageName>, Arc<LanguageSettings>>,
|
||||
accent_data: Option<AccentData>,
|
||||
bracket_fetched_tree_sitter_chunks: HashMap<Range<text::Anchor>, HashSet<Range<BufferRow>>>,
|
||||
semantic_token_state: SemanticTokenState,
|
||||
|
|
@ -9961,6 +9961,13 @@ impl Editor {
|
|||
self.update_edit_prediction_settings(cx);
|
||||
cx.notify();
|
||||
}
|
||||
multi_buffer::Event::SettingsChanged => {
|
||||
let new_language_settings = self.fetch_applicable_language_settings(cx);
|
||||
if new_language_settings != self.applicable_language_settings {
|
||||
self.applicable_language_settings = new_language_settings;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
|
||||
multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
|
||||
multi_buffer::Event::FileHandleChanged => {
|
||||
|
|
@ -10033,7 +10040,7 @@ impl Editor {
|
|||
fn fetch_applicable_language_settings(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> HashMap<Option<LanguageName>, LanguageSettings> {
|
||||
) -> HashMap<Option<LanguageName>, Arc<LanguageSettings>> {
|
||||
if !self.mode.is_full() {
|
||||
return HashMap::default();
|
||||
}
|
||||
|
|
@ -10044,7 +10051,7 @@ impl Editor {
|
|||
let buffer = buffer.read(cx);
|
||||
let language = buffer.language().map(|language| language.name());
|
||||
if let hash_map::Entry::Vacant(v) = acc.entry(language) {
|
||||
v.insert(LanguageSettings::for_buffer(&buffer, cx).into_owned());
|
||||
v.insert(LanguageSettings::for_buffer(buffer, cx));
|
||||
}
|
||||
acc
|
||||
},
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
ranges.push((
|
||||
language_settings.clone(),
|
||||
language_settings,
|
||||
Point::new(current_range_start, 0)
|
||||
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
|
||||
current_range_indent,
|
||||
|
|
|
|||
|
|
@ -1139,7 +1139,7 @@ pub(crate) async fn build_buffer(
|
|||
line_ending,
|
||||
text,
|
||||
);
|
||||
let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite);
|
||||
let mut buffer = Buffer::build(buffer, Some(blob), Capability::ReadWrite, cx);
|
||||
buffer.set_language_async(language, cx);
|
||||
buffer
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,11 +38,10 @@ use gpui::{
|
|||
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::Mutex;
|
||||
use settings::WorktreeId;
|
||||
use settings::{SettingsStore, WorktreeId};
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
cell::Cell,
|
||||
cmp::{self, Ordering, Reverse},
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
|
|
@ -139,6 +138,8 @@ pub struct Buffer {
|
|||
change_bits: Vec<rc::Weak<Cell<bool>>>,
|
||||
modeline: Option<Arc<ModelineSettings>>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
resolved_settings: Option<Arc<LanguageSettings>>,
|
||||
_settings_observer: Option<gpui::Subscription>,
|
||||
tree_sitter_data: Arc<TreeSitterData>,
|
||||
encoding: &'static Encoding,
|
||||
has_bom: bool,
|
||||
|
|
@ -195,6 +196,7 @@ pub struct BufferSnapshot {
|
|||
non_text_state_update_count: usize,
|
||||
pub capability: Capability,
|
||||
modeline: Option<Arc<ModelineSettings>>,
|
||||
resolved_settings: Option<Arc<LanguageSettings>>,
|
||||
}
|
||||
|
||||
/// The kind and amount of indentation in a particular line. For now,
|
||||
|
|
@ -337,6 +339,8 @@ pub enum BufferEvent {
|
|||
LanguageChanged(bool),
|
||||
/// The buffer's syntax trees were updated.
|
||||
Reparsed,
|
||||
/// The buffer's resolved language settings were changed.
|
||||
SettingsChanged,
|
||||
/// The buffer's diagnostics were updated.
|
||||
DiagnosticsUpdated,
|
||||
/// The buffer gained or lost editing capabilities.
|
||||
|
|
@ -963,7 +967,7 @@ pub enum AutoIndentExclusion {
|
|||
|
||||
impl Buffer {
|
||||
/// Create a new buffer with the given base text.
|
||||
pub fn local<T: Into<String>>(base_text: T, cx: &Context<Self>) -> Self {
|
||||
pub fn local<T: Into<String>>(base_text: T, cx: &mut Context<Self>) -> Self {
|
||||
Self::build(
|
||||
TextBuffer::new(
|
||||
ReplicaId::LOCAL,
|
||||
|
|
@ -972,6 +976,7 @@ impl Buffer {
|
|||
),
|
||||
None,
|
||||
Capability::ReadWrite,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -979,7 +984,7 @@ impl Buffer {
|
|||
pub fn local_normalized(
|
||||
base_text_normalized: Rope,
|
||||
line_ending: LineEnding,
|
||||
cx: &Context<Self>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::build(
|
||||
TextBuffer::new_normalized(
|
||||
|
|
@ -990,6 +995,7 @@ impl Buffer {
|
|||
),
|
||||
None,
|
||||
Capability::ReadWrite,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -999,11 +1005,13 @@ impl Buffer {
|
|||
replica_id: ReplicaId,
|
||||
capability: Capability,
|
||||
base_text: impl Into<String>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::build(
|
||||
TextBuffer::new(replica_id, remote_id, base_text.into()),
|
||||
None,
|
||||
capability,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -1014,10 +1022,11 @@ impl Buffer {
|
|||
capability: Capability,
|
||||
message: proto::BufferState,
|
||||
file: Option<Arc<dyn File>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<Self> {
|
||||
let buffer_id = BufferId::new(message.id).context("Could not deserialize buffer_id")?;
|
||||
let buffer = TextBuffer::new(replica_id, buffer_id, message.base_text);
|
||||
let mut this = Self::build(buffer, file, capability);
|
||||
let mut this = Self::build(buffer, file, capability, cx);
|
||||
this.text.set_line_ending(proto::deserialize_line_ending(
|
||||
rpc::proto::LineEnding::try_from(message.line_ending)
|
||||
.ok()
|
||||
|
|
@ -1114,12 +1123,17 @@ impl Buffer {
|
|||
}
|
||||
|
||||
/// Builds a [`Buffer`] with the given underlying [`TextBuffer`], diff base, [`File`] and [`Capability`].
|
||||
pub fn build(buffer: TextBuffer, file: Option<Arc<dyn File>>, capability: Capability) -> Self {
|
||||
pub fn build(
|
||||
buffer: TextBuffer,
|
||||
file: Option<Arc<dyn File>>,
|
||||
capability: Capability,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let saved_mtime = file.as_ref().and_then(|file| file.disk_state().mtime());
|
||||
let snapshot = buffer.snapshot();
|
||||
let syntax_map = Mutex::new(SyntaxMap::new(&snapshot));
|
||||
let tree_sitter_data = TreeSitterData::new(snapshot);
|
||||
Self {
|
||||
let mut this = Self {
|
||||
saved_mtime,
|
||||
tree_sitter_data: Arc::new(tree_sitter_data),
|
||||
saved_version: buffer.version(),
|
||||
|
|
@ -1157,12 +1171,38 @@ impl Buffer {
|
|||
change_bits: Default::default(),
|
||||
modeline: None,
|
||||
_subscriptions: Vec::new(),
|
||||
resolved_settings: None,
|
||||
_settings_observer: Some(cx.observe_global::<SettingsStore>(|this, cx| {
|
||||
this.refresh_resolved_settings(cx);
|
||||
})),
|
||||
encoding: encoding_rs::UTF_8,
|
||||
has_bom: false,
|
||||
reload_with_encoding_txns: HashMap::default(),
|
||||
};
|
||||
this.resolved_settings = this.compute_resolved_settings(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn compute_resolved_settings(&self, cx: &App) -> Option<Arc<LanguageSettings>> {
|
||||
cx.try_global::<SettingsStore>()?;
|
||||
Some(LanguageSettings::resolve_uncached(self, None, cx))
|
||||
}
|
||||
|
||||
fn refresh_resolved_settings(&mut self, cx: &mut Context<Self>) {
|
||||
let resolved = self.compute_resolved_settings(cx);
|
||||
if resolved.as_deref() != self.resolved_settings.as_deref() {
|
||||
self.resolved_settings = resolved;
|
||||
self.non_text_state_update_count += 1;
|
||||
self.was_changed();
|
||||
cx.emit(BufferEvent::SettingsChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_settings(&self) -> Option<&Arc<LanguageSettings>> {
|
||||
self.resolved_settings.as_ref()
|
||||
}
|
||||
|
||||
#[ztracing::instrument(skip_all)]
|
||||
pub fn build_snapshot(
|
||||
text: Rope,
|
||||
|
|
@ -1194,6 +1234,7 @@ impl Buffer {
|
|||
non_text_state_update_count: 0,
|
||||
capability: Capability::ReadOnly,
|
||||
modeline,
|
||||
resolved_settings: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1221,6 +1262,7 @@ impl Buffer {
|
|||
non_text_state_update_count: 0,
|
||||
capability: Capability::ReadOnly,
|
||||
modeline: None,
|
||||
resolved_settings: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1252,6 +1294,7 @@ impl Buffer {
|
|||
non_text_state_update_count: 0,
|
||||
capability: Capability::ReadOnly,
|
||||
modeline: None,
|
||||
resolved_settings: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1283,6 +1326,7 @@ impl Buffer {
|
|||
non_text_state_update_count: self.non_text_state_update_count,
|
||||
capability: self.capability,
|
||||
modeline: self.modeline.clone(),
|
||||
resolved_settings: self.resolved_settings.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1299,7 +1343,7 @@ impl Buffer {
|
|||
has_conflict: self.has_conflict,
|
||||
has_unsaved_edits: Cell::new(self.has_unsaved_edits.get_mut().clone()),
|
||||
_subscriptions: vec![cx.subscribe(&this, Self::on_base_buffer_event)],
|
||||
..Self::build(self.text.branch(), self.file.clone(), self.capability())
|
||||
..Self::build(self.text.branch(), self.file.clone(), self.capability(), cx)
|
||||
};
|
||||
if let Some(language_registry) = self.language_registry() {
|
||||
branch.set_language_registry(language_registry);
|
||||
|
|
@ -1510,6 +1554,7 @@ impl Buffer {
|
|||
self.non_text_state_update_count += 1;
|
||||
self.syntax_map.lock().clear(&self.text);
|
||||
let old_language = std::mem::replace(&mut self.language, language);
|
||||
self.refresh_resolved_settings(cx);
|
||||
self.was_changed();
|
||||
self.reparse(cx, may_block);
|
||||
let has_fresh_language =
|
||||
|
|
@ -1545,9 +1590,14 @@ impl Buffer {
|
|||
}
|
||||
|
||||
/// Assign the buffer [`ModelineSettings`].
|
||||
pub fn set_modeline(&mut self, modeline: Option<ModelineSettings>) -> bool {
|
||||
pub fn set_modeline(
|
||||
&mut self,
|
||||
modeline: Option<ModelineSettings>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
if modeline.as_ref() != self.modeline.as_deref() {
|
||||
self.modeline = modeline.map(Arc::new);
|
||||
self.refresh_resolved_settings(cx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
|
@ -1733,6 +1783,7 @@ impl Buffer {
|
|||
|
||||
self.file = Some(new_file);
|
||||
if file_changed {
|
||||
self.refresh_resolved_settings(cx);
|
||||
self.was_changed();
|
||||
self.non_text_state_update_count += 1;
|
||||
if was_dirty != self.is_dirty() {
|
||||
|
|
@ -4177,6 +4228,10 @@ impl BufferSnapshot {
|
|||
self.modeline.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn resolved_settings(&self) -> Option<&Arc<LanguageSettings>> {
|
||||
self.resolved_settings.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the main [`Language`].
|
||||
pub fn language(&self) -> Option<&Arc<Language>> {
|
||||
self.language.as_ref()
|
||||
|
|
@ -4194,7 +4249,7 @@ impl BufferSnapshot {
|
|||
&'a self,
|
||||
position: D,
|
||||
cx: &'a App,
|
||||
) -> Cow<'a, LanguageSettings> {
|
||||
) -> Arc<LanguageSettings> {
|
||||
LanguageSettings::for_buffer_snapshot(self, Some(position.to_offset(self)), cx)
|
||||
}
|
||||
|
||||
|
|
@ -5430,6 +5485,7 @@ impl Clone for BufferSnapshot {
|
|||
non_text_state_update_count: self.non_text_state_update_count,
|
||||
capability: self.capability,
|
||||
modeline: self.modeline.clone(),
|
||||
resolved_settings: self.resolved_settings.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ fn test_set_line_ending(cx: &mut TestAppContext) {
|
|||
Capability::ReadWrite,
|
||||
base.read(cx).to_proto(cx),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
|
@ -574,6 +575,7 @@ fn test_edit_events(cx: &mut gpui::App) {
|
|||
ReplicaId::new(1),
|
||||
Capability::ReadWrite,
|
||||
"abcdef",
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let buffer1_ops = Arc::new(Mutex::new(Vec::new()));
|
||||
|
|
@ -3844,7 +3846,7 @@ fn test_serialization(cx: &mut gpui::App) {
|
|||
.block_on(buffer1.read(cx).serialize_ops(None, cx));
|
||||
let buffer2 = cx.new(|cx| {
|
||||
let mut buffer =
|
||||
Buffer::from_proto(ReplicaId::new(1), Capability::ReadWrite, state, None).unwrap();
|
||||
Buffer::from_proto(ReplicaId::new(1), Capability::ReadWrite, state, None, cx).unwrap();
|
||||
buffer.apply_ops(
|
||||
ops.into_iter()
|
||||
.map(|op| proto::deserialize_operation(op).unwrap()),
|
||||
|
|
@ -3868,6 +3870,7 @@ fn test_branch_and_merge(cx: &mut TestAppContext) {
|
|||
Capability::ReadWrite,
|
||||
base.read(cx).to_proto(cx),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
|
@ -4180,9 +4183,14 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
|
|||
let ops = cx
|
||||
.foreground_executor()
|
||||
.block_on(base_buffer.read(cx).serialize_ops(None, cx));
|
||||
let mut buffer =
|
||||
Buffer::from_proto(ReplicaId::new(i as u16), Capability::ReadWrite, state, None)
|
||||
.unwrap();
|
||||
let mut buffer = Buffer::from_proto(
|
||||
ReplicaId::new(i as u16),
|
||||
Capability::ReadWrite,
|
||||
state,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
buffer.apply_ops(
|
||||
ops.into_iter()
|
||||
.map(|op| proto::deserialize_operation(op).unwrap()),
|
||||
|
|
@ -4311,6 +4319,7 @@ fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
|
|||
Capability::ReadWrite,
|
||||
old_buffer_state,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
new_buffer.apply_ops(
|
||||
|
|
@ -5194,6 +5203,7 @@ fn test_completion_triggers_across_language_servers(cx: &mut TestAppContext) {
|
|||
Capability::ReadWrite,
|
||||
buffer.read(cx).to_proto(cx),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
|
@ -5317,6 +5327,66 @@ fn init_settings(cx: &mut App, f: fn(&mut AllLanguageSettingsContent)) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_settings_changed_event(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| init_settings(cx, |_| {}));
|
||||
|
||||
let buffer = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx));
|
||||
let settings_change_count = std::rc::Rc::new(std::cell::Cell::new(0));
|
||||
let subscription = cx.update(|cx| {
|
||||
cx.subscribe(&buffer, {
|
||||
let settings_change_count = settings_change_count.clone();
|
||||
move |_, event, _| {
|
||||
if let BufferEvent::SettingsChanged = event {
|
||||
settings_change_count.set(settings_change_count.get() + 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| {
|
||||
crate::language_settings::LanguageSettings::for_buffer(buffer, cx)
|
||||
.tab_size
|
||||
.get()
|
||||
}),
|
||||
4
|
||||
);
|
||||
assert_eq!(settings_change_count.get(), 0);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings(cx, |content| {
|
||||
content.project.all_languages.defaults.tab_size = Some(3.try_into().unwrap());
|
||||
});
|
||||
});
|
||||
});
|
||||
assert_eq!(settings_change_count.get(), 1);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, cx| {
|
||||
crate::language_settings::LanguageSettings::for_buffer(buffer, cx)
|
||||
.tab_size
|
||||
.get()
|
||||
}),
|
||||
3
|
||||
);
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings(cx, |content| {
|
||||
content.project.all_languages.defaults.tab_size = Some(3.try_into().unwrap());
|
||||
});
|
||||
});
|
||||
});
|
||||
assert_eq!(
|
||||
settings_change_count.get(),
|
||||
1,
|
||||
"a no-op settings update should not emit SettingsChanged"
|
||||
);
|
||||
|
||||
drop(subscription);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) {
|
||||
use util::RandomCharIter;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ pub use settings::{
|
|||
};
|
||||
use settings::{RegisterSetting, Settings, SettingsLocation, SettingsStore, merge_from::MergeFrom};
|
||||
use shellexpand;
|
||||
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc, time::Duration};
|
||||
use std::{num::NonZeroU32, path::Path, sync::Arc, time::Duration};
|
||||
use text::ToOffset;
|
||||
|
||||
/// Returns the settings for all languages from the provided file.
|
||||
|
|
@ -44,8 +44,8 @@ pub fn all_language_settings<'a>(
|
|||
pub struct AllLanguageSettings {
|
||||
/// The edit prediction settings.
|
||||
pub edit_predictions: EditPredictionSettings,
|
||||
pub defaults: LanguageSettings,
|
||||
languages: HashMap<LanguageName, LanguageSettings>,
|
||||
pub defaults: Arc<LanguageSettings>,
|
||||
languages: HashMap<LanguageName, Arc<LanguageSettings>>,
|
||||
pub file_types: FxHashMap<Arc<str>, (GlobSet, Vec<String>)>,
|
||||
}
|
||||
|
||||
|
|
@ -277,35 +277,41 @@ pub struct PrettierSettings {
|
|||
}
|
||||
|
||||
impl LanguageSettings {
|
||||
pub fn for_buffer<'a>(buffer: &'a Buffer, cx: &'a App) -> Cow<'a, LanguageSettings> {
|
||||
pub fn for_buffer(buffer: &Buffer, cx: &App) -> Arc<LanguageSettings> {
|
||||
Self::resolve(Some(buffer), None, cx)
|
||||
}
|
||||
|
||||
pub fn for_buffer_at<'a, D: ToOffset>(
|
||||
buffer: &'a Buffer,
|
||||
pub fn for_buffer_at<D: ToOffset>(
|
||||
buffer: &Buffer,
|
||||
position: D,
|
||||
cx: &'a App,
|
||||
) -> Cow<'a, LanguageSettings> {
|
||||
cx: &App,
|
||||
) -> Arc<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,
|
||||
pub fn for_buffer_snapshot(
|
||||
buffer: &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(),
|
||||
});
|
||||
|
||||
cx: &App,
|
||||
) -> Arc<LanguageSettings> {
|
||||
let language = if let Some(offset) = offset {
|
||||
buffer.language_at(offset)
|
||||
} else {
|
||||
buffer.language()
|
||||
};
|
||||
|
||||
if let Some(resolved) = buffer.resolved_settings()
|
||||
&& language == buffer.language()
|
||||
{
|
||||
return Arc::clone(resolved);
|
||||
}
|
||||
|
||||
let location = buffer.file().map(|f| SettingsLocation {
|
||||
worktree_id: f.worktree_id(cx),
|
||||
path: f.path().as_ref(),
|
||||
});
|
||||
|
||||
let mut settings = AllLanguageSettings::get(location, cx).language(
|
||||
location,
|
||||
language.map(|l| l.name()).as_ref(),
|
||||
|
|
@ -313,33 +319,48 @@ impl LanguageSettings {
|
|||
);
|
||||
|
||||
if let Some(modeline) = buffer.modeline() {
|
||||
merge_with_modeline(settings.to_mut(), modeline);
|
||||
merge_with_modeline(Arc::make_mut(&mut settings), modeline);
|
||||
}
|
||||
|
||||
settings
|
||||
}
|
||||
|
||||
pub fn resolve<'a>(
|
||||
buffer: Option<&'a Buffer>,
|
||||
pub fn resolve(
|
||||
buffer: Option<&Buffer>,
|
||||
override_language: Option<&LanguageName>,
|
||||
cx: &'a App,
|
||||
) -> Cow<'a, LanguageSettings> {
|
||||
cx: &App,
|
||||
) -> Arc<LanguageSettings> {
|
||||
let Some(buffer) = buffer else {
|
||||
return AllLanguageSettings::get(None, cx).language(None, override_language, cx);
|
||||
};
|
||||
|
||||
if let Some(resolved) = buffer.resolved_settings()
|
||||
&& override_language.is_none_or(|language| {
|
||||
Some(language) == buffer.language().map(|l| l.name()).as_ref()
|
||||
})
|
||||
{
|
||||
return resolved.clone();
|
||||
}
|
||||
|
||||
Self::resolve_uncached(buffer, override_language, cx)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_uncached(
|
||||
buffer: &Buffer,
|
||||
override_language: Option<&LanguageName>,
|
||||
cx: &App,
|
||||
) -> Arc<LanguageSettings> {
|
||||
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)
|
||||
};
|
||||
let buffer_language = buffer.language().map(|l| l.name());
|
||||
let language_name = override_language.or(buffer_language.as_ref());
|
||||
let mut settings =
|
||||
AllLanguageSettings::get(location, cx).language(location, language_name, cx);
|
||||
|
||||
if let Some(modeline) = buffer.modeline() {
|
||||
merge_with_modeline(settings.to_mut(), modeline);
|
||||
merge_with_modeline(Arc::make_mut(&mut settings), modeline);
|
||||
}
|
||||
|
||||
settings
|
||||
|
|
@ -643,12 +664,12 @@ impl From<EditPredictionPromptFormatContent> for EditPredictionPromptFormat {
|
|||
|
||||
impl AllLanguageSettings {
|
||||
/// Returns the [`LanguageSettings`] for the language with the specified name.
|
||||
pub fn language<'a>(
|
||||
&'a self,
|
||||
location: Option<SettingsLocation<'a>>,
|
||||
pub fn language(
|
||||
&self,
|
||||
location: Option<SettingsLocation<'_>>,
|
||||
language_name: Option<&LanguageName>,
|
||||
cx: &'a App,
|
||||
) -> Cow<'a, LanguageSettings> {
|
||||
cx: &App,
|
||||
) -> Arc<LanguageSettings> {
|
||||
let settings = language_name
|
||||
.and_then(|name| self.languages.get(name))
|
||||
.unwrap_or(&self.defaults);
|
||||
|
|
@ -660,11 +681,11 @@ impl AllLanguageSettings {
|
|||
.properties(location.worktree_id, location.path)
|
||||
});
|
||||
if let Some(editorconfig_properties) = editorconfig_properties {
|
||||
let mut settings = settings.clone();
|
||||
let mut settings = (**settings).clone();
|
||||
merge_with_editorconfig(&mut settings, &editorconfig_properties);
|
||||
Cow::Owned(settings)
|
||||
Arc::new(settings)
|
||||
} else {
|
||||
Cow::Borrowed(settings)
|
||||
Arc::clone(settings)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -873,7 +894,7 @@ impl settings::Settings for AllLanguageSettings {
|
|||
}
|
||||
}
|
||||
|
||||
let default_language_settings = load_from_content(all_languages.defaults.clone());
|
||||
let default_language_settings = Arc::new(load_from_content(all_languages.defaults.clone()));
|
||||
|
||||
let mut languages = HashMap::default();
|
||||
for (language_name, settings) in &all_languages.languages.0 {
|
||||
|
|
@ -881,7 +902,7 @@ impl settings::Settings for AllLanguageSettings {
|
|||
settings::merge_from::MergeFrom::merge_from(&mut language_settings, settings);
|
||||
languages.insert(
|
||||
LanguageName(language_name.clone().into()),
|
||||
load_from_content(language_settings),
|
||||
Arc::new(load_from_content(language_settings)),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ pub enum Event {
|
|||
Reloaded,
|
||||
CapabilityChanged,
|
||||
LanguageChanged(BufferId, bool),
|
||||
SettingsChanged,
|
||||
Reparsed(BufferId),
|
||||
Saved,
|
||||
FileHandleChanged,
|
||||
|
|
@ -2039,6 +2040,7 @@ impl MultiBuffer {
|
|||
BufferEvent::LanguageChanged(has_language) => {
|
||||
Event::LanguageChanged(buffer_id, *has_language)
|
||||
}
|
||||
BufferEvent::SettingsChanged => Event::SettingsChanged,
|
||||
BufferEvent::Reparsed => Event::Reparsed(buffer_id),
|
||||
BufferEvent::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
||||
BufferEvent::CapabilityChanged => {
|
||||
|
|
@ -2164,7 +2166,7 @@ impl MultiBuffer {
|
|||
.and_then(|(buffer, offset)| buffer.read(cx).language_at(offset))
|
||||
}
|
||||
|
||||
pub fn language_settings<'a>(&'a self, cx: &'a App) -> Cow<'a, LanguageSettings> {
|
||||
pub fn language_settings(&self, cx: &App) -> Arc<LanguageSettings> {
|
||||
let snapshot = self.snapshot(cx);
|
||||
snapshot
|
||||
.excerpts
|
||||
|
|
@ -2174,15 +2176,11 @@ impl MultiBuffer {
|
|||
.unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::default(), cx))
|
||||
}
|
||||
|
||||
pub fn language_settings_at<'a, T: ToOffset>(
|
||||
&'a self,
|
||||
point: T,
|
||||
cx: &'a App,
|
||||
) -> Cow<'a, LanguageSettings> {
|
||||
pub fn language_settings_at<T: ToOffset>(&self, point: T, cx: &App) -> Arc<LanguageSettings> {
|
||||
if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) {
|
||||
LanguageSettings::for_buffer_at(buffer.read(cx), offset, cx)
|
||||
} else {
|
||||
Cow::Borrowed(&AllLanguageSettings::get_global(cx).defaults)
|
||||
AllLanguageSettings::get_global(cx).defaults.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6202,7 +6200,7 @@ impl MultiBufferSnapshot {
|
|||
.and_then(|(buffer, offset)| buffer.language_at(offset))
|
||||
}
|
||||
|
||||
fn language_settings<'a>(&'a self, cx: &'a App) -> Cow<'a, LanguageSettings> {
|
||||
fn language_settings(&self, cx: &App) -> Arc<LanguageSettings> {
|
||||
self.excerpts
|
||||
.first()
|
||||
.map(|excerpt| excerpt.buffer_snapshot(self))
|
||||
|
|
@ -6210,15 +6208,11 @@ impl MultiBufferSnapshot {
|
|||
.unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::ZERO, cx))
|
||||
}
|
||||
|
||||
pub fn language_settings_at<'a, T: ToOffset>(
|
||||
&'a self,
|
||||
point: T,
|
||||
cx: &'a App,
|
||||
) -> Cow<'a, LanguageSettings> {
|
||||
pub fn language_settings_at<T: ToOffset>(&self, point: T, cx: &App) -> Arc<LanguageSettings> {
|
||||
if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
|
||||
buffer.settings_at(offset, cx)
|
||||
} else {
|
||||
Cow::Borrowed(&AllLanguageSettings::get_global(cx).defaults)
|
||||
AllLanguageSettings::get_global(cx).defaults.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,9 +102,14 @@ fn test_remote(cx: &mut App) {
|
|||
let ops = cx
|
||||
.foreground_executor()
|
||||
.block_on(host_buffer.read(cx).serialize_ops(None, cx));
|
||||
let mut buffer =
|
||||
Buffer::from_proto(ReplicaId::REMOTE_SERVER, Capability::ReadWrite, state, None)
|
||||
.unwrap();
|
||||
let mut buffer = Buffer::from_proto(
|
||||
ReplicaId::REMOTE_SERVER,
|
||||
Capability::ReadWrite,
|
||||
state,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
buffer.apply_ops(
|
||||
ops.into_iter()
|
||||
.map(|op| language::proto::deserialize_operation(op).unwrap()),
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ impl RemoteBufferStore {
|
|||
proto::create_buffer_for_peer::Variant::State(mut state) => {
|
||||
let buffer_id = BufferId::new(state.id)?;
|
||||
|
||||
let buffer_result = maybe!({
|
||||
let buffer_file_result = maybe!({
|
||||
let mut buffer_file = None;
|
||||
if let Some(file) = state.file.take() {
|
||||
let worktree_id = worktree::WorktreeId::from_proto(file.worktree_id);
|
||||
|
|
@ -188,12 +188,15 @@ impl RemoteBufferStore {
|
|||
buffer_file = Some(Arc::new(File::from_proto(file, worktree, cx)?)
|
||||
as Arc<dyn language::File>);
|
||||
}
|
||||
Buffer::from_proto(replica_id, capability, state, buffer_file)
|
||||
anyhow::Ok(buffer_file)
|
||||
});
|
||||
|
||||
match buffer_result {
|
||||
Ok(buffer) => {
|
||||
let buffer = cx.new(|_| buffer);
|
||||
match buffer_file_result {
|
||||
Ok(buffer_file) => {
|
||||
let buffer = cx.new(|cx| {
|
||||
Buffer::from_proto(replica_id, capability, state, buffer_file, cx)
|
||||
.expect("buffer_id was validated above")
|
||||
});
|
||||
self.loading_remote_buffers_by_id.insert(buffer_id, buffer);
|
||||
}
|
||||
Err(error) => {
|
||||
|
|
@ -704,8 +707,9 @@ impl LocalBufferStore {
|
|||
)
|
||||
})
|
||||
.await;
|
||||
cx.insert_entity(reservation, |_| {
|
||||
let mut buffer = Buffer::build(text_buffer, Some(loaded.file), capability);
|
||||
cx.insert_entity(reservation, |cx| {
|
||||
let mut buffer =
|
||||
Buffer::build(text_buffer, Some(loaded.file), capability, cx);
|
||||
buffer.set_encoding(loaded.encoding);
|
||||
buffer.set_has_bom(loaded.has_bom);
|
||||
buffer
|
||||
|
|
@ -725,6 +729,7 @@ impl LocalBufferStore {
|
|||
is_private: false,
|
||||
})),
|
||||
Capability::ReadWrite,
|
||||
cx,
|
||||
);
|
||||
apply_initial_line_ending(&mut buffer, cx);
|
||||
buffer
|
||||
|
|
|
|||
|
|
@ -1619,7 +1619,7 @@ impl LocalLspStore {
|
|||
.map(|(adapter, lsp)| (adapter.clone(), lsp.clone()))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let settings = LanguageSettings::for_buffer(buffer, cx).into_owned();
|
||||
let settings = LanguageSettings::for_buffer(buffer, cx);
|
||||
let request_timeout = ProjectSettings::get_global(cx)
|
||||
.global_lsp_settings
|
||||
.get_request_timeout();
|
||||
|
|
@ -4953,10 +4953,41 @@ impl LspStore {
|
|||
self.on_buffer_reloaded(buffer, cx);
|
||||
}
|
||||
|
||||
language::BufferEvent::SettingsChanged => {
|
||||
self.on_buffer_settings_changed(&buffer, cx);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_buffer_settings_changed(&mut self, buffer: &Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) else {
|
||||
return;
|
||||
};
|
||||
let buffer = buffer.read(cx);
|
||||
if buffer.language().is_none() {
|
||||
return;
|
||||
}
|
||||
let settings = LanguageSettings::for_buffer(buffer, cx);
|
||||
if settings.prettier.allowed
|
||||
&& let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings)
|
||||
{
|
||||
let worktree_id = File::from_dyn(buffer.file()).map(|file| file.worktree_id(cx));
|
||||
let prettier_plugins = prettier_plugins
|
||||
.iter()
|
||||
.map(|plugin| Arc::from(plugin.as_str()))
|
||||
.collect::<Vec<_>>();
|
||||
prettier_store.update(cx, |prettier_store, cx| {
|
||||
prettier_store.install_default_prettier(
|
||||
worktree_id,
|
||||
prettier_plugins.into_iter(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn on_buffer_added(&mut self, buffer: &Entity<Buffer>, cx: &mut Context<Self>) -> Result<()> {
|
||||
buffer
|
||||
.read(cx)
|
||||
|
|
@ -5334,7 +5365,7 @@ impl LspStore {
|
|||
|
||||
log::debug!("Parsed modeline settings: {:?}", modeline_settings);
|
||||
|
||||
buffer_handle.update(cx, |buffer, _cx| buffer.set_modeline(modeline_settings))
|
||||
buffer_handle.update(cx, |buffer, cx| buffer.set_modeline(modeline_settings, cx))
|
||||
}
|
||||
|
||||
fn detect_language_for_buffer(
|
||||
|
|
@ -5408,8 +5439,7 @@ impl LspStore {
|
|||
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 {
|
||||
|
|
@ -5770,27 +5800,8 @@ impl LspStore {
|
|||
}
|
||||
|
||||
fn on_settings_changed(&mut self, cx: &mut Context<Self>) {
|
||||
let mut language_formatters_to_check = Vec::new();
|
||||
for buffer in self.buffer_store.read(cx).buffers() {
|
||||
let buffer = buffer.read(cx);
|
||||
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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.request_workspace_config_refresh();
|
||||
|
||||
if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) {
|
||||
prettier_store.update(cx, |prettier_store, cx| {
|
||||
prettier_store.on_settings_changed(language_formatters_to_check, cx)
|
||||
})
|
||||
}
|
||||
|
||||
let new_semantic_token_rules = crate::project_settings::ProjectSettings::get_global(cx)
|
||||
.global_lsp_settings
|
||||
.semantic_token_rules
|
||||
|
|
|
|||
|
|
@ -696,31 +696,6 @@ impl PrettierStore {
|
|||
not_installed_plugins: plugins_to_install,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn on_settings_changed(
|
||||
&mut self,
|
||||
language_formatters_to_check: Vec<(Option<WorktreeId>, LanguageSettings)>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let mut prettier_plugins_by_worktree = HashMap::default();
|
||||
for (worktree, language_settings) in language_formatters_to_check {
|
||||
if language_settings.prettier.allowed
|
||||
&& let Some(plugins) = prettier_plugins_for_language(&language_settings)
|
||||
{
|
||||
prettier_plugins_by_worktree
|
||||
.entry(worktree)
|
||||
.or_insert_with(HashSet::default)
|
||||
.extend(plugins.iter().cloned());
|
||||
}
|
||||
}
|
||||
for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
|
||||
self.install_default_prettier(
|
||||
worktree,
|
||||
prettier_plugins.into_iter().map(Arc::from),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prettier_plugins_for_language(
|
||||
|
|
|
|||
|
|
@ -326,14 +326,14 @@ async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
|
|||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let settings_for = async |path: &str, cx: &mut TestAppContext| -> LanguageSettings {
|
||||
let settings_for = async |path: &str, cx: &mut TestAppContext| -> Arc<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| LanguageSettings::for_buffer(&buffer.read(cx), cx))
|
||||
};
|
||||
|
||||
let settings_a = settings_for("a.rs", cx).await;
|
||||
|
|
@ -411,14 +411,14 @@ 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 settings_for = async |path: &str, cx: &mut TestAppContext| -> Arc<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| LanguageSettings::for_buffer(&buffer.read(cx), cx))
|
||||
};
|
||||
|
||||
let settings_rs = settings_for("main.rs", cx).await;
|
||||
|
|
@ -468,7 +468,7 @@ async fn test_internal_editorconfig_root_stops_traversal(cx: &mut gpui::TestAppC
|
|||
.await
|
||||
.unwrap();
|
||||
cx.update(|cx| {
|
||||
let settings = LanguageSettings::for_buffer(buffer.read(cx), cx).into_owned();
|
||||
let settings = LanguageSettings::for_buffer(buffer.read(cx), cx);
|
||||
assert_eq!(Some(settings.tab_size), NonZeroU32::new(2));
|
||||
});
|
||||
}
|
||||
|
|
@ -757,7 +757,7 @@ async fn test_adding_worktree_discovers_external_editorconfigs(cx: &mut gpui::Te
|
|||
.unwrap();
|
||||
|
||||
cx.update(|cx| {
|
||||
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx).into_owned();
|
||||
let settings = LanguageSettings::for_buffer(&buffer.read(cx), cx);
|
||||
|
||||
// Test existing worktree has tab_size = 7
|
||||
assert_eq!(Some(settings.tab_size), NonZeroU32::new(7));
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
use anyhow::{Context as _, Result};
|
||||
use collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use ec4rs::{ConfigParser, PropertiesSource, Section};
|
||||
use ec4rs::{
|
||||
ConfigParser, PropertyKey as _, Section,
|
||||
property::{
|
||||
EndOfLine, FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs,
|
||||
},
|
||||
};
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use gpui::{Context, EventEmitter, Task};
|
||||
use paths::EDITORCONFIG_NAME;
|
||||
use smallvec::SmallVec;
|
||||
use std::{path::Path, str::FromStr, sync::Arc};
|
||||
use util::{ResultExt as _, rel_path::RelPath};
|
||||
use util::rel_path::RelPath;
|
||||
|
||||
use crate::{InvalidSettingsError, LocalSettingsPath, WorktreeId, watch_config_file};
|
||||
|
||||
|
|
@ -323,8 +328,14 @@ impl EditorconfigStore {
|
|||
for_worktree: WorktreeId,
|
||||
for_path: &RelPath,
|
||||
) -> Option<EditorconfigProperties> {
|
||||
let mut properties = EditorconfigProperties::new();
|
||||
let state = self.worktree_state.get(&for_worktree);
|
||||
let has_internal_configs = state.is_some_and(|state| !state.internal_configs.is_empty());
|
||||
let has_external_configs = self.external_configs(for_worktree).next().is_some();
|
||||
if !has_internal_configs && !has_external_configs {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut properties = EditorconfigProperties::new();
|
||||
let internal_root_config_is_root = state
|
||||
.and_then(|state| state.internal_configs.get(RelPath::empty()))
|
||||
.and_then(|data| data.1.as_ref())
|
||||
|
|
@ -339,7 +350,7 @@ impl EditorconfigStore {
|
|||
properties = EditorconfigProperties::new();
|
||||
}
|
||||
for section in &parsed_editorconfig.sections {
|
||||
section.apply_to(&mut properties, std_path).log_err()?;
|
||||
apply_relevant_properties(section, std_path, &mut properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -363,7 +374,7 @@ impl EditorconfigStore {
|
|||
properties = EditorconfigProperties::new();
|
||||
}
|
||||
for section in &config.sections {
|
||||
section.apply_to(&mut properties, std_path).log_err()?;
|
||||
apply_relevant_properties(section, std_path, &mut properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -373,6 +384,30 @@ impl EditorconfigStore {
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_relevant_properties(
|
||||
section: &Section,
|
||||
std_path: &Path,
|
||||
properties: &mut EditorconfigProperties,
|
||||
) {
|
||||
if !section.applies_to(std_path) {
|
||||
return;
|
||||
}
|
||||
let relevant_keys = [
|
||||
IndentStyle::key(),
|
||||
IndentSize::key(),
|
||||
TabWidth::key(),
|
||||
EndOfLine::key(),
|
||||
MaxLineLen::key(),
|
||||
FinalNewline::key(),
|
||||
TrimTrailingWs::key(),
|
||||
];
|
||||
for (key, value) in section.props().iter() {
|
||||
if relevant_keys.contains(&key) {
|
||||
properties.insert_raw_for_key(key, value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl EditorconfigStore {
|
||||
pub fn test_state(&self) -> (Vec<WorktreeId>, Vec<Arc<Path>>, Vec<Arc<Path>>) {
|
||||
|
|
@ -393,3 +428,113 @@ impl EditorconfigStore {
|
|||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_properties_resolution_and_updates() {
|
||||
let mut store = EditorconfigStore::default();
|
||||
let worktree_id = WorktreeId::from_usize(1);
|
||||
let root = LocalSettingsPath::InWorktree(Arc::from(RelPath::empty()));
|
||||
let file_path = RelPath::from_unix_str("src/main.rs").unwrap();
|
||||
|
||||
assert_eq!(store.properties(worktree_id, file_path), None);
|
||||
|
||||
store
|
||||
.set_configs(
|
||||
worktree_id,
|
||||
root.clone(),
|
||||
Some("root = true\n\n[*]\nindent_size = 4\n"),
|
||||
)
|
||||
.unwrap();
|
||||
let properties = store.properties(worktree_id, file_path).unwrap();
|
||||
assert_eq!(
|
||||
properties.get::<IndentSize>().ok(),
|
||||
Some(IndentSize::Value(4))
|
||||
);
|
||||
|
||||
store
|
||||
.set_configs(
|
||||
worktree_id,
|
||||
root.clone(),
|
||||
Some("root = true\n\n[*]\nindent_size = 8\n"),
|
||||
)
|
||||
.unwrap();
|
||||
let properties = store.properties(worktree_id, file_path).unwrap();
|
||||
assert_eq!(
|
||||
properties.get::<IndentSize>().ok(),
|
||||
Some(IndentSize::Value(8))
|
||||
);
|
||||
|
||||
store.set_configs(worktree_id, root, None).unwrap();
|
||||
assert_eq!(store.properties(worktree_id, file_path), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nested_config_overrides_and_irrelevant_keys() {
|
||||
let mut store = EditorconfigStore::default();
|
||||
let worktree_id = WorktreeId::from_usize(1);
|
||||
let file_path = RelPath::from_unix_str("src/main.rs").unwrap();
|
||||
|
||||
store
|
||||
.set_configs(
|
||||
worktree_id,
|
||||
LocalSettingsPath::InWorktree(Arc::from(RelPath::empty())),
|
||||
Some(
|
||||
"root = true\n\n[*]\nindent_size = 4\nmax_line_length = 100\ncurly_bracket_next_line = true\n",
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
store
|
||||
.set_configs(
|
||||
worktree_id,
|
||||
LocalSettingsPath::InWorktree(Arc::from(RelPath::from_unix_str("src").unwrap())),
|
||||
Some("[*.rs]\nindent_size = 2\n"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let properties = store.properties(worktree_id, file_path).unwrap();
|
||||
assert_eq!(
|
||||
properties.get::<IndentSize>().ok(),
|
||||
Some(IndentSize::Value(2))
|
||||
);
|
||||
assert_eq!(
|
||||
properties.get::<MaxLineLen>().ok(),
|
||||
Some(MaxLineLen::Value(100))
|
||||
);
|
||||
assert_eq!(
|
||||
properties
|
||||
.get_raw_for_key("curly_bracket_next_line")
|
||||
.into_option(),
|
||||
None
|
||||
);
|
||||
|
||||
let other_file = RelPath::from_unix_str("src/main.js").unwrap();
|
||||
let properties = store.properties(worktree_id, other_file).unwrap();
|
||||
assert_eq!(
|
||||
properties.get::<IndentSize>().ok(),
|
||||
Some(IndentSize::Value(4))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_properties_after_worktree_removal() {
|
||||
let mut store = EditorconfigStore::default();
|
||||
let worktree_id = WorktreeId::from_usize(1);
|
||||
let file_path = RelPath::from_unix_str("src/main.rs").unwrap();
|
||||
|
||||
store
|
||||
.set_configs(
|
||||
worktree_id,
|
||||
LocalSettingsPath::InWorktree(Arc::from(RelPath::empty())),
|
||||
Some("root = true\n\n[*]\nindent_size = 4\n"),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(store.properties(worktree_id, file_path).is_some());
|
||||
|
||||
store.remove_for_worktree(worktree_id);
|
||||
assert_eq!(store.properties(worktree_id, file_path), None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue