Compare commits

...

10 Commits

Author SHA1 Message Date
Nate Butler
58a7a277df wip 2024-10-21 10:20:26 -04:00
Nate Butler
5e9f084f12 Start on quick commit UI PoC 2024-10-21 09:33:15 -04:00
Michael Sloan
b355a6f449 Fix markdown preview handling of empty list items (#19449)
Before this change, `parse_block` was consuming events that it doesn't
handle. This was fine in its use in `parse_document`, but in its use in
`parse_list` this broke when there is an empty list item, causing it to
consume list end tags / list item starts / etc.

Release Notes:

- Fixed markdown preview rendering of empty list items.
2024-10-21 15:13:26 +02:00
reslear
6341ad2f7a docs: Correct link to Vue extension (#19508)
Closes -

Release Notes:

- N/A
2024-10-21 08:12:10 -04:00
Kirill Bulatov
d3cb08bf35 Support .editorconfig (#19455)
Closes https://github.com/zed-industries/zed/issues/8534
Supersedes https://github.com/zed-industries/zed/pull/16349

Potential concerns:
* we do not follow up to the `/` when looking for `.editorconfig`, only
up to the worktree root.
Seems fine for most of the cases, and the rest should be solved
generically later, as the same issue exists for settings.json
* `fn language` in `AllLanguageSettings` is very hot, called very
frequently during rendering. We accumulate and parse all `.editorconfig`
file contents beforehand, but have to go over globs and match these
against the path given + merge the properties still.
This does not seem to be very bad, but needs more testing and
potentially some extra caching.


Release Notes:

- Added .editorconfig support

---------

Co-authored-by: Ulysse Buonomo <buonomo.ulysse@gmail.com>
2024-10-21 13:05:30 +03:00
renovate[bot]
d95a4f8671 Update swatinem/rust-cache digest to 82a92a6 (#19318)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [swatinem/rust-cache](https://redirect.github.com/swatinem/rust-cache)
| action | digest | `23bce25` -> `82a92a6` |

---

### Configuration

📅 **Schedule**: Branch creation - "after 3pm on Wednesday" in timezone
America/New_York, Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

Release Notes:

- N/A

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOC4xMjAuMSIsInVwZGF0ZWRJblZlciI6IjM4LjEyMC4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-21 11:10:45 +03:00
Alvaro Gaona
44dc693d30 docs: Update C# and C++ configuration pages (#19500)
- Update C# configuration page.
- Fix typo in C++ configuration page.

Release Notes:

- N/A
2024-10-21 11:07:12 +03:00
Conrad Irwin
92c29be74c SSH Remoting: Fix reconnects (#19485)
Before this change messages could be lost on reconnect, now they will
not be.

Release Notes:

- SSH Remoting: make reconnects smoother

---------

Co-authored-by: Nathan <nathan@zed.dev>
2024-10-19 23:14:19 -06:00
Kirill Bulatov
1ae30f5813 Show project panel symlink icons for remote clients (#19464) 2024-10-19 19:44:47 +03:00
Antonio Scandurra
e8207288e5 Refold updated patch only if the patch was already folded (#19462)
Release Notes:

- N/A
2024-10-19 17:10:50 +02:00
54 changed files with 1657 additions and 517 deletions

View File

@@ -138,7 +138,7 @@ jobs:
clean: false clean: false
- name: Cache dependencies - name: Cache dependencies
uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2 uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet" cache-provider: "buildjet"
@@ -170,7 +170,7 @@ jobs:
clean: false clean: false
- name: Cache dependencies - name: Cache dependencies
uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2 uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "buildjet" cache-provider: "buildjet"
@@ -193,7 +193,7 @@ jobs:
clean: false clean: false
- name: Cache dependencies - name: Cache dependencies
uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2 uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github" cache-provider: "github"

View File

@@ -21,7 +21,7 @@ jobs:
clean: false clean: false
- name: Cache dependencies - name: Cache dependencies
uses: swatinem/rust-cache@23bce251a8cd2ffc3c1075eaa2367cf899916d84 # v2 uses: swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # v2
with: with:
save-if: ${{ github.ref == 'refs/heads/main' }} save-if: ${{ github.ref == 'refs/heads/main' }}
cache-provider: "github" cache-provider: "github"

11
Cargo.lock generated
View File

@@ -3649,6 +3649,12 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
[[package]]
name = "ec4rs"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acf65d056c7da9c971c2847ce250fd1f0f9659d5718845c3ec0ad95f5668352c"
[[package]] [[package]]
name = "ecdsa" name = "ecdsa"
version = "0.14.8" version = "0.14.8"
@@ -6210,6 +6216,7 @@ dependencies = [
"clock", "clock",
"collections", "collections",
"ctor", "ctor",
"ec4rs",
"env_logger", "env_logger",
"futures 0.3.30", "futures 0.3.30",
"fuzzy", "fuzzy",
@@ -9119,6 +9126,7 @@ name = "remote"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait",
"collections", "collections",
"fs", "fs",
"futures 0.3.30", "futures 0.3.30",
@@ -10301,6 +10309,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"collections", "collections",
"ec4rs",
"fs", "fs",
"futures 0.3.30", "futures 0.3.30",
"gpui", "gpui",
@@ -12595,9 +12604,11 @@ version = "0.1.0"
dependencies = [ dependencies = [
"editor", "editor",
"gpui", "gpui",
"menu",
"settings", "settings",
"theme", "theme",
"ui", "ui",
"workspace",
] ]
[[package]] [[package]]

View File

@@ -347,6 +347,7 @@ ctor = "0.2.6"
dashmap = "6.0" dashmap = "6.0"
derive_more = "0.99.17" derive_more = "0.99.17"
dirs = "4.0" dirs = "4.0"
ec4rs = "1.1"
emojis = "0.6.1" emojis = "0.6.1"
env_logger = "0.11" env_logger = "0.11"
exec = "0.3.1" exec = "0.3.1"

View File

@@ -2219,6 +2219,7 @@ impl ContextEditor {
merge_adjacent: false, merge_adjacent: false,
}; };
let should_refold;
if let Some(state) = self.patches.get_mut(&range) { if let Some(state) = self.patches.get_mut(&range) {
replaced_blocks.insert(state.footer_block_id, render_block); replaced_blocks.insert(state.footer_block_id, render_block);
if let Some(editor_state) = &state.editor { if let Some(editor_state) = &state.editor {
@@ -2233,6 +2234,9 @@ impl ContextEditor {
}); });
} }
} }
should_refold =
snapshot.intersects_fold(patch_start.to_offset(&snapshot.buffer_snapshot));
} else { } else {
let block_ids = editor.insert_blocks( let block_ids = editor.insert_blocks(
[BlockProperties { [BlockProperties {
@@ -2266,10 +2270,14 @@ impl ContextEditor {
update_task: None, update_task: None,
}, },
); );
should_refold = true;
} }
editor.unfold_ranges([patch_start..patch_end], true, false, cx); if should_refold {
editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx); editor.unfold_ranges([patch_start..patch_end], true, false, cx);
editor.fold_ranges([(patch_start..patch_end, header_placeholder)], false, cx);
}
} }
editor.remove_creases(removed_crease_ids, cx); editor.remove_creases(removed_crease_ids, cx);

View File

@@ -78,10 +78,10 @@ CREATE TABLE "worktree_entries" (
"id" INTEGER NOT NULL, "id" INTEGER NOT NULL,
"is_dir" BOOL NOT NULL, "is_dir" BOOL NOT NULL,
"path" VARCHAR NOT NULL, "path" VARCHAR NOT NULL,
"canonical_path" TEXT,
"inode" INTEGER NOT NULL, "inode" INTEGER NOT NULL,
"mtime_seconds" INTEGER NOT NULL, "mtime_seconds" INTEGER NOT NULL,
"mtime_nanos" INTEGER NOT NULL, "mtime_nanos" INTEGER NOT NULL,
"is_symlink" BOOL NOT NULL,
"is_external" BOOL NOT NULL, "is_external" BOOL NOT NULL,
"is_ignored" BOOL NOT NULL, "is_ignored" BOOL NOT NULL,
"is_deleted" BOOL NOT NULL, "is_deleted" BOOL NOT NULL,

View File

@@ -0,0 +1,2 @@
ALTER TABLE worktree_entries ADD COLUMN canonical_path text;
ALTER TABLE worktree_entries ALTER COLUMN is_symlink SET DEFAULT false;

View File

@@ -317,7 +317,7 @@ impl Database {
inode: ActiveValue::set(entry.inode as i64), inode: ActiveValue::set(entry.inode as i64),
mtime_seconds: ActiveValue::set(mtime.seconds as i64), mtime_seconds: ActiveValue::set(mtime.seconds as i64),
mtime_nanos: ActiveValue::set(mtime.nanos as i32), mtime_nanos: ActiveValue::set(mtime.nanos as i32),
is_symlink: ActiveValue::set(entry.is_symlink), canonical_path: ActiveValue::set(entry.canonical_path.clone()),
is_ignored: ActiveValue::set(entry.is_ignored), is_ignored: ActiveValue::set(entry.is_ignored),
is_external: ActiveValue::set(entry.is_external), is_external: ActiveValue::set(entry.is_external),
git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)), git_status: ActiveValue::set(entry.git_status.map(|status| status as i64)),
@@ -338,7 +338,7 @@ impl Database {
worktree_entry::Column::Inode, worktree_entry::Column::Inode,
worktree_entry::Column::MtimeSeconds, worktree_entry::Column::MtimeSeconds,
worktree_entry::Column::MtimeNanos, worktree_entry::Column::MtimeNanos,
worktree_entry::Column::IsSymlink, worktree_entry::Column::CanonicalPath,
worktree_entry::Column::IsIgnored, worktree_entry::Column::IsIgnored,
worktree_entry::Column::GitStatus, worktree_entry::Column::GitStatus,
worktree_entry::Column::ScanId, worktree_entry::Column::ScanId,
@@ -735,7 +735,7 @@ impl Database {
seconds: db_entry.mtime_seconds as u64, seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32, nanos: db_entry.mtime_nanos as u32,
}), }),
is_symlink: db_entry.is_symlink, canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored, is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external, is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32), git_status: db_entry.git_status.map(|status| status as i32),

View File

@@ -659,7 +659,7 @@ impl Database {
seconds: db_entry.mtime_seconds as u64, seconds: db_entry.mtime_seconds as u64,
nanos: db_entry.mtime_nanos as u32, nanos: db_entry.mtime_nanos as u32,
}), }),
is_symlink: db_entry.is_symlink, canonical_path: db_entry.canonical_path,
is_ignored: db_entry.is_ignored, is_ignored: db_entry.is_ignored,
is_external: db_entry.is_external, is_external: db_entry.is_external,
git_status: db_entry.git_status.map(|status| status as i32), git_status: db_entry.git_status.map(|status| status as i32),

View File

@@ -16,12 +16,12 @@ pub struct Model {
pub mtime_seconds: i64, pub mtime_seconds: i64,
pub mtime_nanos: i32, pub mtime_nanos: i32,
pub git_status: Option<i64>, pub git_status: Option<i64>,
pub is_symlink: bool,
pub is_ignored: bool, pub is_ignored: bool,
pub is_external: bool, pub is_external: bool,
pub is_deleted: bool, pub is_deleted: bool,
pub scan_id: i64, pub scan_id: i64,
pub is_fifo: bool, pub is_fifo: bool,
pub canonical_path: Option<String>,
} }
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@@ -2237,7 +2237,7 @@ fn join_project_internal(
worktree_id: worktree.id, worktree_id: worktree.id,
path: settings_file.path, path: settings_file.path,
content: Some(settings_file.content), content: Some(settings_file.content),
kind: Some(proto::update_user_settings::Kind::Settings.into()), kind: Some(settings_file.kind.to_proto() as i32),
}, },
)?; )?;
} }

View File

@@ -12,6 +12,7 @@ use editor::{
test::editor_test_context::{AssertionContextManager, EditorTestContext}, test::editor_test_context::{AssertionContextManager, EditorTestContext},
Editor, Editor,
}; };
use fs::Fs;
use futures::StreamExt; use futures::StreamExt;
use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext}; use gpui::{TestAppContext, UpdateGlobal, VisualContext, VisualTestContext};
use indoc::indoc; use indoc::indoc;
@@ -30,7 +31,7 @@ use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use std::{ use std::{
ops::Range, ops::Range,
path::Path, path::{Path, PathBuf},
sync::{ sync::{
atomic::{self, AtomicBool, AtomicUsize}, atomic::{self, AtomicBool, AtomicUsize},
Arc, Arc,
@@ -60,7 +61,7 @@ async fn test_host_disconnect(
.fs() .fs()
.insert_tree( .insert_tree(
"/a", "/a",
serde_json::json!({ json!({
"a.txt": "a-contents", "a.txt": "a-contents",
"b.txt": "b-contents", "b.txt": "b-contents",
}), }),
@@ -2152,6 +2153,295 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
}); });
} }
#[gpui::test(iterations = 30)]
async fn test_collaborating_with_editorconfig(
cx_a: &mut TestAppContext,
cx_b: &mut TestAppContext,
) {
let mut server = TestServer::start(cx_a.executor()).await;
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
server
.create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
.await;
let active_call_a = cx_a.read(ActiveCall::global);
cx_b.update(editor::init);
// Set up a fake language server.
client_a.language_registry().add(rust_lang());
client_a
.fs()
.insert_tree(
"/a",
json!({
"src": {
"main.rs": "mod other;\nfn main() { let foo = other::foo(); }",
"other_mod": {
"other.rs": "pub fn foo() -> usize {\n 4\n}",
".editorconfig": "",
},
},
".editorconfig": "[*]\ntab_width = 2\n",
}),
)
.await;
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
let project_id = active_call_a
.update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
.await
.unwrap();
let main_buffer_a = project_a
.update(cx_a, |p, cx| {
p.open_buffer((worktree_id, "src/main.rs"), cx)
})
.await
.unwrap();
let other_buffer_a = project_a
.update(cx_a, |p, cx| {
p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
})
.await
.unwrap();
let cx_a = cx_a.add_empty_window();
let main_editor_a =
cx_a.new_view(|cx| Editor::for_buffer(main_buffer_a, Some(project_a.clone()), cx));
let other_editor_a =
cx_a.new_view(|cx| Editor::for_buffer(other_buffer_a, Some(project_a), cx));
let mut main_editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
window: cx_a.handle(),
editor: main_editor_a,
assertion_cx: AssertionContextManager::new(),
};
let mut other_editor_cx_a = EditorTestContext {
cx: cx_a.clone(),
window: cx_a.handle(),
editor: other_editor_a,
assertion_cx: AssertionContextManager::new(),
};
// Join the project as client B.
let project_b = client_b.join_remote_project(project_id, cx_b).await;
let main_buffer_b = project_b
.update(cx_b, |p, cx| {
p.open_buffer((worktree_id, "src/main.rs"), cx)
})
.await
.unwrap();
let other_buffer_b = project_b
.update(cx_b, |p, cx| {
p.open_buffer((worktree_id, "src/other_mod/other.rs"), cx)
})
.await
.unwrap();
let cx_b = cx_b.add_empty_window();
let main_editor_b =
cx_b.new_view(|cx| Editor::for_buffer(main_buffer_b, Some(project_b.clone()), cx));
let other_editor_b =
cx_b.new_view(|cx| Editor::for_buffer(other_buffer_b, Some(project_b.clone()), cx));
let mut main_editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
window: cx_b.handle(),
editor: main_editor_b,
assertion_cx: AssertionContextManager::new(),
};
let mut other_editor_cx_b = EditorTestContext {
cx: cx_b.clone(),
window: cx_b.handle(),
editor: other_editor_b,
assertion_cx: AssertionContextManager::new(),
};
let initial_main = indoc! {"
ˇmod other;
fn main() { let foo = other::foo(); }"};
let initial_other = indoc! {"
ˇpub fn foo() -> usize {
4
}"};
let first_tabbed_main = indoc! {"
ˇmod other;
fn main() { let foo = other::foo(); }"};
tab_undo_assert(
&mut main_editor_cx_a,
&mut main_editor_cx_b,
initial_main,
first_tabbed_main,
true,
);
tab_undo_assert(
&mut main_editor_cx_a,
&mut main_editor_cx_b,
initial_main,
first_tabbed_main,
false,
);
let first_tabbed_other = indoc! {"
ˇpub fn foo() -> usize {
4
}"};
tab_undo_assert(
&mut other_editor_cx_a,
&mut other_editor_cx_b,
initial_other,
first_tabbed_other,
true,
);
tab_undo_assert(
&mut other_editor_cx_a,
&mut other_editor_cx_b,
initial_other,
first_tabbed_other,
false,
);
client_a
.fs()
.atomic_write(
PathBuf::from("/a/src/.editorconfig"),
"[*]\ntab_width = 3\n".to_owned(),
)
.await
.unwrap();
cx_a.run_until_parked();
cx_b.run_until_parked();
let second_tabbed_main = indoc! {"
ˇmod other;
fn main() { let foo = other::foo(); }"};
tab_undo_assert(
&mut main_editor_cx_a,
&mut main_editor_cx_b,
initial_main,
second_tabbed_main,
true,
);
tab_undo_assert(
&mut main_editor_cx_a,
&mut main_editor_cx_b,
initial_main,
second_tabbed_main,
false,
);
let second_tabbed_other = indoc! {"
ˇpub fn foo() -> usize {
4
}"};
tab_undo_assert(
&mut other_editor_cx_a,
&mut other_editor_cx_b,
initial_other,
second_tabbed_other,
true,
);
tab_undo_assert(
&mut other_editor_cx_a,
&mut other_editor_cx_b,
initial_other,
second_tabbed_other,
false,
);
let editorconfig_buffer_b = project_b
.update(cx_b, |p, cx| {
p.open_buffer((worktree_id, "src/other_mod/.editorconfig"), cx)
})
.await
.unwrap();
editorconfig_buffer_b.update(cx_b, |buffer, cx| {
buffer.set_text("[*.rs]\ntab_width = 6\n", cx);
});
project_b
.update(cx_b, |project, cx| {
project.save_buffer(editorconfig_buffer_b.clone(), cx)
})
.await
.unwrap();
cx_a.run_until_parked();
cx_b.run_until_parked();
tab_undo_assert(
&mut main_editor_cx_a,
&mut main_editor_cx_b,
initial_main,
second_tabbed_main,
true,
);
tab_undo_assert(
&mut main_editor_cx_a,
&mut main_editor_cx_b,
initial_main,
second_tabbed_main,
false,
);
let third_tabbed_other = indoc! {"
ˇpub fn foo() -> usize {
4
}"};
tab_undo_assert(
&mut other_editor_cx_a,
&mut other_editor_cx_b,
initial_other,
third_tabbed_other,
true,
);
tab_undo_assert(
&mut other_editor_cx_a,
&mut other_editor_cx_b,
initial_other,
third_tabbed_other,
false,
);
}
#[track_caller]
fn tab_undo_assert(
cx_a: &mut EditorTestContext,
cx_b: &mut EditorTestContext,
expected_initial: &str,
expected_tabbed: &str,
a_tabs: bool,
) {
cx_a.assert_editor_state(expected_initial);
cx_b.assert_editor_state(expected_initial);
if a_tabs {
cx_a.update_editor(|editor, cx| {
editor.tab(&editor::actions::Tab, cx);
});
} else {
cx_b.update_editor(|editor, cx| {
editor.tab(&editor::actions::Tab, cx);
});
}
cx_a.run_until_parked();
cx_b.run_until_parked();
cx_a.assert_editor_state(expected_tabbed);
cx_b.assert_editor_state(expected_tabbed);
if a_tabs {
cx_a.update_editor(|editor, cx| {
editor.undo(&editor::actions::Undo, cx);
});
} else {
cx_b.update_editor(|editor, cx| {
editor.undo(&editor::actions::Undo, cx);
});
}
cx_a.run_until_parked();
cx_b.run_until_parked();
cx_a.assert_editor_state(expected_initial);
cx_b.assert_editor_state(expected_initial);
}
fn extract_hint_labels(editor: &Editor) -> Vec<String> { fn extract_hint_labels(editor: &Editor) -> Vec<String> {
let mut labels = Vec::new(); let mut labels = Vec::new();
for hint in editor.inlay_hint_cache().hints() { for hint in editor.inlay_hint_cache().hints() {

View File

@@ -34,7 +34,7 @@ use project::{
}; };
use rand::prelude::*; use rand::prelude::*;
use serde_json::json; use serde_json::json;
use settings::{LocalSettingsKind, SettingsStore}; use settings::SettingsStore;
use std::{ use std::{
cell::{Cell, RefCell}, cell::{Cell, RefCell},
env, future, mem, env, future, mem,
@@ -3328,16 +3328,8 @@ async fn test_local_settings(
.local_settings(worktree_b.read(cx).id()) .local_settings(worktree_b.read(cx).id())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
&[ &[
( (Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
Path::new("").into(), (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
LocalSettingsKind::Settings,
r#"{"tab_size":2}"#.to_string()
),
(
Path::new("a").into(),
LocalSettingsKind::Settings,
r#"{"tab_size":8}"#.to_string()
),
] ]
) )
}); });
@@ -3355,16 +3347,8 @@ async fn test_local_settings(
.local_settings(worktree_b.read(cx).id()) .local_settings(worktree_b.read(cx).id())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
&[ &[
( (Path::new("").into(), r#"{}"#.to_string()),
Path::new("").into(), (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
LocalSettingsKind::Settings,
r#"{}"#.to_string()
),
(
Path::new("a").into(),
LocalSettingsKind::Settings,
r#"{"tab_size":8}"#.to_string()
),
] ]
) )
}); });
@@ -3392,16 +3376,8 @@ async fn test_local_settings(
.local_settings(worktree_b.read(cx).id()) .local_settings(worktree_b.read(cx).id())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
&[ &[
( (Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
Path::new("a").into(), (Path::new("b").into(), r#"{"tab_size":4}"#.to_string()),
LocalSettingsKind::Settings,
r#"{"tab_size":8}"#.to_string()
),
(
Path::new("b").into(),
LocalSettingsKind::Settings,
r#"{"tab_size":4}"#.to_string()
),
] ]
) )
}); });
@@ -3431,11 +3407,7 @@ async fn test_local_settings(
store store
.local_settings(worktree_b.read(cx).id()) .local_settings(worktree_b.read(cx).id())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
&[( &[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
Path::new("a").into(),
LocalSettingsKind::Settings,
r#"{"hard_tabs":true}"#.to_string()
),]
) )
}); });
} }

View File

@@ -3,7 +3,7 @@ use call::ActiveCall;
use fs::{FakeFs, Fs as _}; use fs::{FakeFs, Fs as _};
use gpui::{Context as _, TestAppContext}; use gpui::{Context as _, TestAppContext};
use http_client::BlockedHttpClient; use http_client::BlockedHttpClient;
use language::{language_settings::all_language_settings, LanguageRegistry}; use language::{language_settings::language_settings, LanguageRegistry};
use node_runtime::NodeRuntime; use node_runtime::NodeRuntime;
use project::ProjectPath; use project::ProjectPath;
use remote::SshRemoteClient; use remote::SshRemoteClient;
@@ -26,7 +26,7 @@ async fn test_sharing_an_ssh_remote_project(
.await; .await;
// Set up project on remote FS // Set up project on remote FS
let (client_ssh, server_ssh) = SshRemoteClient::fake(cx_a, server_cx); let (port, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx);
let remote_fs = FakeFs::new(server_cx.executor()); let remote_fs = FakeFs::new(server_cx.executor());
remote_fs remote_fs
.insert_tree( .insert_tree(
@@ -67,6 +67,7 @@ async fn test_sharing_an_ssh_remote_project(
) )
}); });
let client_ssh = SshRemoteClient::fake_client(port, cx_a).await;
let (project_a, worktree_id) = client_a let (project_a, worktree_id) = client_a
.build_ssh_project("/code/project1", client_ssh, cx_a) .build_ssh_project("/code/project1", client_ssh, cx_a)
.await; .await;
@@ -134,9 +135,7 @@ async fn test_sharing_an_ssh_remote_project(
cx_b.read(|cx| { cx_b.read(|cx| {
let file = buffer_b.read(cx).file(); let file = buffer_b.read(cx).file();
assert_eq!( assert_eq!(
all_language_settings(file, cx) language_settings(Some("Rust".into()), file, cx).language_servers,
.language(Some(&("Rust".into())))
.language_servers,
["override-rust-analyzer".to_string()] ["override-rust-analyzer".to_string()]
) )
}); });

View File

@@ -864,7 +864,11 @@ impl Copilot {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
let uri = registered_buffer.uri.clone(); let uri = registered_buffer.uri.clone();
let position = position.to_point_utf16(buffer); let position = position.to_point_utf16(buffer);
let settings = language_settings(buffer.language_at(position).as_ref(), buffer.file(), cx); let settings = language_settings(
buffer.language_at(position).map(|l| l.name()),
buffer.file(),
cx,
);
let tab_size = settings.tab_size; let tab_size = settings.tab_size;
let hard_tabs = settings.hard_tabs; let hard_tabs = settings.hard_tabs;
let relative_path = buffer let relative_path = buffer

View File

@@ -77,7 +77,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
let file = buffer.file(); let file = buffer.file();
let language = buffer.language_at(cursor_position); let language = buffer.language_at(cursor_position);
let settings = all_language_settings(file, cx); let settings = all_language_settings(file, cx);
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref())) settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
} }
fn refresh( fn refresh(
@@ -209,7 +209,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
) { ) {
let settings = AllLanguageSettings::get_global(cx); let settings = AllLanguageSettings::get_global(cx);
let copilot_enabled = settings.inline_completions_enabled(None, None); let copilot_enabled = settings.inline_completions_enabled(None, None, cx);
if !copilot_enabled { if !copilot_enabled {
return; return;

View File

@@ -423,11 +423,12 @@ impl DisplayMap {
} }
fn tab_size(buffer: &Model<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 { fn tab_size(buffer: &Model<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
let language = buffer let language = buffer
.read(cx) .and_then(|buffer| buffer.language())
.as_singleton() .map(|l| l.name());
.and_then(|buffer| buffer.read(cx).language()); let file = buffer.and_then(|buffer| buffer.file());
language_settings(language, None, cx).tab_size language_settings(language, file, cx).tab_size
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -90,7 +90,7 @@ pub use inline_completion_provider::*;
pub use items::MAX_TAB_TITLE_LEN; pub use items::MAX_TAB_TITLE_LEN;
use itertools::Itertools; use itertools::Itertools;
use language::{ use language::{
language_settings::{self, all_language_settings, InlayHintSettings}, language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
Point, Selection, SelectionGoal, TransactionId, Point, Selection, SelectionGoal, TransactionId,
@@ -428,8 +428,7 @@ impl Default for EditorStyle {
} }
pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle { pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle {
let show_background = all_language_settings(None, cx) let show_background = language_settings::language_settings(None, None, cx)
.language(None)
.inlay_hints .inlay_hints
.show_background; .show_background;
@@ -4248,7 +4247,10 @@ impl Editor {
.text_anchor_for_position(position, cx)?; .text_anchor_for_position(position, cx)?;
let settings = language_settings::language_settings( let settings = language_settings::language_settings(
buffer.read(cx).language_at(buffer_position).as_ref(), buffer
.read(cx)
.language_at(buffer_position)
.map(|l| l.name()),
buffer.read(cx).file(), buffer.read(cx).file(),
cx, cx,
); );
@@ -6282,11 +6284,9 @@ impl Editor {
let project_path = buffer.read(cx).project_path(cx)?; let project_path = buffer.read(cx).project_path(cx)?;
let project = self.project.as_ref()?.read(cx); let project = self.project.as_ref()?.read(cx);
let entry = project.entry_for_path(&project_path, cx)?; let entry = project.entry_for_path(&project_path, cx)?;
let abs_path = project.absolute_path(&project_path, cx)?; let parent = match &entry.canonical_path {
let parent = if entry.is_symlink { Some(canonical_path) => canonical_path.to_path_buf(),
abs_path.canonicalize().ok()? None => project.absolute_path(&project_path, cx)?,
} else {
abs_path
} }
.parent()? .parent()?
.to_path_buf(); .to_path_buf();
@@ -13376,11 +13376,8 @@ fn inlay_hint_settings(
cx: &mut ViewContext<'_, Editor>, cx: &mut ViewContext<'_, Editor>,
) -> InlayHintSettings { ) -> InlayHintSettings {
let file = snapshot.file_at(location); let file = snapshot.file_at(location);
let language = snapshot.language_at(location); let language = snapshot.language_at(location).map(|l| l.name());
let settings = all_language_settings(file, cx); language_settings(language, file, cx).inlay_hints
settings
.language(language.map(|l| l.name()).as_ref())
.inlay_hints
} }
fn consume_contiguous_rows( fn consume_contiguous_rows(

View File

@@ -39,9 +39,13 @@ impl Editor {
) -> Option<Vec<MultiBufferIndentGuide>> { ) -> Option<Vec<MultiBufferIndentGuide>> {
let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| { let show_indent_guides = self.should_show_indent_guides().unwrap_or_else(|| {
if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(buffer) = self.buffer().read(cx).as_singleton() {
language_settings(buffer.read(cx).language(), buffer.read(cx).file(), cx) language_settings(
.indent_guides buffer.read(cx).language().map(|l| l.name()),
.enabled buffer.read(cx).file(),
cx,
)
.indent_guides
.enabled
} else { } else {
true true
} }

View File

@@ -356,8 +356,11 @@ impl ExtensionImports for WasmState {
cx.update(|cx| match category.as_str() { cx.update(|cx| match category.as_str() {
"language" => { "language" => {
let key = key.map(|k| LanguageName::new(&k)); let key = key.map(|k| LanguageName::new(&k));
let settings = let settings = AllLanguageSettings::get(location, cx).language(
AllLanguageSettings::get(location, cx).language(key.as_ref()); location,
key.as_ref(),
cx,
);
Ok(serde_json::to_string(&settings::LanguageSettings { Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size, tab_size: settings.tab_size,
})?) })?)

View File

@@ -402,8 +402,11 @@ impl ExtensionImports for WasmState {
cx.update(|cx| match category.as_str() { cx.update(|cx| match category.as_str() {
"language" => { "language" => {
let key = key.map(|k| LanguageName::new(&k)); let key = key.map(|k| LanguageName::new(&k));
let settings = let settings = AllLanguageSettings::get(location, cx).language(
AllLanguageSettings::get(location, cx).language(key.as_ref()); location,
key.as_ref(),
cx,
);
Ok(serde_json::to_string(&settings::LanguageSettings { Ok(serde_json::to_string(&settings::LanguageSettings {
tab_size: settings.tab_size, tab_size: settings.tab_size,
})?) })?)

View File

@@ -62,7 +62,7 @@ impl Render for InlineCompletionButton {
let status = copilot.read(cx).status(); let status = copilot.read(cx).status();
let enabled = self.editor_enabled.unwrap_or_else(|| { let enabled = self.editor_enabled.unwrap_or_else(|| {
all_language_settings.inline_completions_enabled(None, None) all_language_settings.inline_completions_enabled(None, None, cx)
}); });
let icon = match status { let icon = match status {
@@ -248,8 +248,9 @@ impl InlineCompletionButton {
if let Some(language) = self.language.clone() { if let Some(language) = self.language.clone() {
let fs = fs.clone(); let fs = fs.clone();
let language_enabled = language_settings::language_settings(Some(&language), None, cx) let language_enabled =
.show_inline_completions; language_settings::language_settings(Some(language.name()), None, cx)
.show_inline_completions;
menu = menu.entry( menu = menu.entry(
format!( format!(
@@ -292,7 +293,7 @@ impl InlineCompletionButton {
); );
} }
let globally_enabled = settings.inline_completions_enabled(None, None); let globally_enabled = settings.inline_completions_enabled(None, None, cx);
menu.entry( menu.entry(
if globally_enabled { if globally_enabled {
"Hide Inline Completions for All Files" "Hide Inline Completions for All Files"
@@ -340,6 +341,7 @@ impl InlineCompletionButton {
&& all_language_settings(file, cx).inline_completions_enabled( && all_language_settings(file, cx).inline_completions_enabled(
language, language,
file.map(|file| file.path().as_ref()), file.map(|file| file.path().as_ref()),
cx,
), ),
) )
}; };
@@ -442,7 +444,7 @@ async fn configure_disabled_globs(
fn toggle_inline_completions_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) { fn toggle_inline_completions_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
let show_inline_completions = let show_inline_completions =
all_language_settings(None, cx).inline_completions_enabled(None, None); all_language_settings(None, cx).inline_completions_enabled(None, None, cx);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| { update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
file.defaults.show_inline_completions = Some(!show_inline_completions) file.defaults.show_inline_completions = Some(!show_inline_completions)
}); });
@@ -466,7 +468,7 @@ fn toggle_inline_completions_for_language(
cx: &mut AppContext, cx: &mut AppContext,
) { ) {
let show_inline_completions = let show_inline_completions =
all_language_settings(None, cx).inline_completions_enabled(Some(&language), None); all_language_settings(None, cx).inline_completions_enabled(Some(&language), None, cx);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| { update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
file.languages file.languages
.entry(language.name()) .entry(language.name())

View File

@@ -30,6 +30,7 @@ async-trait.workspace = true
async-watch.workspace = true async-watch.workspace = true
clock.workspace = true clock.workspace = true
collections.workspace = true collections.workspace = true
ec4rs.workspace = true
futures.workspace = true futures.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
git.workspace = true git.workspace = true

View File

@@ -37,6 +37,7 @@ use smallvec::SmallVec;
use smol::future::yield_now; use smol::future::yield_now;
use std::{ use std::{
any::Any, any::Any,
borrow::Cow,
cell::Cell, cell::Cell,
cmp::{self, Ordering, Reverse}, cmp::{self, Ordering, Reverse},
collections::BTreeMap, collections::BTreeMap,
@@ -2490,7 +2491,11 @@ impl BufferSnapshot {
/// Returns [`IndentSize`] for a given position that respects user settings /// Returns [`IndentSize`] for a given position that respects user settings
/// and language preferences. /// and language preferences.
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize { pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
let settings = language_settings(self.language_at(position), self.file(), cx); let settings = language_settings(
self.language_at(position).map(|l| l.name()),
self.file(),
cx,
);
if settings.hard_tabs { if settings.hard_tabs {
IndentSize::tab() IndentSize::tab()
} else { } else {
@@ -2823,11 +2828,15 @@ impl BufferSnapshot {
/// Returns the settings for the language at the given location. /// Returns the settings for the language at the given location.
pub fn settings_at<'a, D: ToOffset>( pub fn settings_at<'a, D: ToOffset>(
&self, &'a self,
position: D, position: D,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> Cow<'a, LanguageSettings> {
language_settings(self.language_at(position), self.file.as_ref(), cx) language_settings(
self.language_at(position).map(|l| l.name()),
self.file.as_ref(),
cx,
)
} }
pub fn char_classifier_at<T: ToOffset>(&self, point: T) -> CharClassifier { pub fn char_classifier_at<T: ToOffset>(&self, point: T) -> CharClassifier {
@@ -3529,7 +3538,8 @@ impl BufferSnapshot {
ignore_disabled_for_language: bool, ignore_disabled_for_language: bool,
cx: &AppContext, cx: &AppContext,
) -> Vec<IndentGuide> { ) -> Vec<IndentGuide> {
let language_settings = language_settings(self.language(), self.file.as_ref(), cx); let language_settings =
language_settings(self.language().map(|l| l.name()), self.file.as_ref(), cx);
let settings = language_settings.indent_guides; let settings = language_settings.indent_guides;
if !ignore_disabled_for_language && !settings.enabled { if !ignore_disabled_for_language && !settings.enabled {
return Vec::new(); return Vec::new();

View File

@@ -4,6 +4,10 @@ use crate::{File, Language, LanguageName, LanguageServerName};
use anyhow::Result; use anyhow::Result;
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use core::slice; use core::slice;
use ec4rs::{
property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
Properties as EditorconfigProperties,
};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use gpui::AppContext; use gpui::AppContext;
use itertools::{Either, Itertools}; use itertools::{Either, Itertools};
@@ -16,8 +20,10 @@ use serde::{
Deserialize, Deserializer, Serialize, Deserialize, Deserializer, Serialize,
}; };
use serde_json::Value; use serde_json::Value;
use settings::{add_references_to_properties, Settings, SettingsLocation, SettingsSources}; use settings::{
use std::{num::NonZeroU32, path::Path, sync::Arc}; add_references_to_properties, Settings, SettingsLocation, SettingsSources, SettingsStore,
};
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
use util::serde::default_true; use util::serde::default_true;
/// Initializes the language settings. /// Initializes the language settings.
@@ -27,17 +33,20 @@ pub fn init(cx: &mut AppContext) {
/// Returns the settings for the specified language from the provided file. /// Returns the settings for the specified language from the provided file.
pub fn language_settings<'a>( pub fn language_settings<'a>(
language: Option<&Arc<Language>>, language: Option<LanguageName>,
file: Option<&Arc<dyn File>>, file: Option<&'a Arc<dyn File>>,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> Cow<'a, LanguageSettings> {
let language_name = language.map(|l| l.name()); let location = file.map(|f| SettingsLocation {
all_language_settings(file, cx).language(language_name.as_ref()) worktree_id: f.worktree_id(cx),
path: f.path().as_ref(),
});
AllLanguageSettings::get(location, cx).language(location, language.as_ref(), cx)
} }
/// Returns the settings for all languages from the provided file. /// Returns the settings for all languages from the provided file.
pub fn all_language_settings<'a>( pub fn all_language_settings<'a>(
file: Option<&Arc<dyn File>>, file: Option<&'a Arc<dyn File>>,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a AllLanguageSettings { ) -> &'a AllLanguageSettings {
let location = file.map(|f| SettingsLocation { let location = file.map(|f| SettingsLocation {
@@ -810,13 +819,27 @@ impl InlayHintSettings {
impl AllLanguageSettings { impl AllLanguageSettings {
/// Returns the [`LanguageSettings`] for the language with the specified name. /// Returns the [`LanguageSettings`] for the language with the specified name.
pub fn language<'a>(&'a self, language_name: Option<&LanguageName>) -> &'a LanguageSettings { pub fn language<'a>(
if let Some(name) = language_name { &'a self,
if let Some(overrides) = self.languages.get(name) { location: Option<SettingsLocation<'a>>,
return overrides; language_name: Option<&LanguageName>,
} cx: &'a AppContext,
) -> Cow<'a, LanguageSettings> {
let settings = language_name
.and_then(|name| self.languages.get(name))
.unwrap_or(&self.defaults);
let editorconfig_properties = location.and_then(|location| {
cx.global::<SettingsStore>()
.editorconfg_properties(location.worktree_id, location.path)
});
if let Some(editorconfig_properties) = editorconfig_properties {
let mut settings = settings.clone();
merge_with_editorconfig(&mut settings, &editorconfig_properties);
Cow::Owned(settings)
} else {
Cow::Borrowed(settings)
} }
&self.defaults
} }
/// Returns whether inline completions are enabled for the given path. /// Returns whether inline completions are enabled for the given path.
@@ -833,6 +856,7 @@ impl AllLanguageSettings {
&self, &self,
language: Option<&Arc<Language>>, language: Option<&Arc<Language>>,
path: Option<&Path>, path: Option<&Path>,
cx: &AppContext,
) -> bool { ) -> bool {
if let Some(path) = path { if let Some(path) = path {
if !self.inline_completions_enabled_for_path(path) { if !self.inline_completions_enabled_for_path(path) {
@@ -840,11 +864,64 @@ impl AllLanguageSettings {
} }
} }
self.language(language.map(|l| l.name()).as_ref()) self.language(None, language.map(|l| l.name()).as_ref(), cx)
.show_inline_completions .show_inline_completions
} }
} }
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
let max_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
MaxLineLen::Value(u) => Some(u as u32),
MaxLineLen::Off => None,
});
let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
IndentSize::Value(u) => NonZeroU32::new(u as u32),
IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
TabWidth::Value(u) => NonZeroU32::new(u as u32),
}),
});
let hard_tabs = cfg
.get::<IndentStyle>()
.map(|v| v.eq(&IndentStyle::Tabs))
.ok();
let ensure_final_newline_on_save = cfg
.get::<FinalNewline>()
.map(|v| match v {
FinalNewline::Value(b) => b,
})
.ok();
let remove_trailing_whitespace_on_save = cfg
.get::<TrimTrailingWs>()
.map(|v| match v {
TrimTrailingWs::Value(b) => b,
})
.ok();
let preferred_line_length = max_line_length;
let soft_wrap = if max_line_length.is_some() {
Some(SoftWrap::PreferredLineLength)
} else {
None
};
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}
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,
);
merge(&mut settings.preferred_line_length, preferred_line_length);
merge(&mut settings.soft_wrap, soft_wrap);
}
/// The kind of an inlay hint. /// The kind of an inlay hint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InlayHintKind { pub enum InlayHintKind {

View File

@@ -6,7 +6,6 @@ use futures::{io::BufReader, StreamExt};
use gpui::{AppContext, AsyncAppContext}; use gpui::{AppContext, AsyncAppContext};
use http_client::github::{latest_github_release, GitHubLspBinaryVersion}; use http_client::github::{latest_github_release, GitHubLspBinaryVersion};
pub use language::*; pub use language::*;
use language_settings::all_language_settings;
use lsp::LanguageServerBinary; use lsp::LanguageServerBinary;
use regex::Regex; use regex::Regex;
use smol::fs::{self, File}; use smol::fs::{self, File};
@@ -21,6 +20,8 @@ use std::{
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName}; use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::{fs::remove_matching, maybe, ResultExt}; use util::{fs::remove_matching, maybe, ResultExt};
use crate::language_settings::language_settings;
pub struct RustLspAdapter; pub struct RustLspAdapter;
impl RustLspAdapter { impl RustLspAdapter {
@@ -424,13 +425,13 @@ impl ContextProvider for RustContextProvider {
cx: &AppContext, cx: &AppContext,
) -> Option<TaskTemplates> { ) -> Option<TaskTemplates> {
const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN"; const DEFAULT_RUN_NAME_STR: &str = "RUST_DEFAULT_PACKAGE_RUN";
let package_to_run = all_language_settings(file.as_ref(), cx) let package_to_run = language_settings(Some("Rust".into()), file.as_ref(), cx)
.language(Some(&"Rust".into()))
.tasks .tasks
.variables .variables
.get(DEFAULT_RUN_NAME_STR); .get(DEFAULT_RUN_NAME_STR)
.cloned();
let run_task_args = if let Some(package_to_run) = package_to_run { let run_task_args = if let Some(package_to_run) = package_to_run {
vec!["run".into(), "-p".into(), package_to_run.clone()] vec!["run".into(), "-p".into(), package_to_run]
} else { } else {
vec!["run".into()] vec!["run".into()]
}; };

View File

@@ -101,7 +101,7 @@ impl LspAdapter for YamlLspAdapter {
let tab_size = cx.update(|cx| { let tab_size = cx.update(|cx| {
AllLanguageSettings::get(Some(location), cx) AllLanguageSettings::get(Some(location), cx)
.language(Some(&"YAML".into())) .language(Some(location), Some(&"YAML".into()), cx)
.tab_size .tab_size
})?; })?;
let mut options = serde_json::json!({"[yaml]": {"editor.tabSize": tab_size}}); let mut options = serde_json::json!({"[yaml]": {"editor.tabSize": tab_size}});

View File

@@ -102,6 +102,8 @@ impl<'a> MarkdownParser<'a> {
while !self.eof() { while !self.eof() {
if let Some(block) = self.parse_block().await { if let Some(block) = self.parse_block().await {
self.parsed.extend(block); self.parsed.extend(block);
} else {
self.cursor += 1;
} }
} }
self self
@@ -163,20 +165,14 @@ impl<'a> MarkdownParser<'a> {
let code_block = self.parse_code_block(language).await; let code_block = self.parse_code_block(language).await;
Some(vec![ParsedMarkdownElement::CodeBlock(code_block)]) Some(vec![ParsedMarkdownElement::CodeBlock(code_block)])
} }
_ => { _ => None,
self.cursor += 1;
None
}
}, },
Event::Rule => { Event::Rule => {
let source_range = source_range.clone(); let source_range = source_range.clone();
self.cursor += 1; self.cursor += 1;
Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)]) Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)])
} }
_ => { _ => None,
self.cursor += 1;
None
}
} }
} }
@@ -1000,6 +996,8 @@ Some other content
- Inner - Inner
- Inner - Inner
2. Goodbyte 2. Goodbyte
- Next item empty
-
* Last * Last
", ",
) )
@@ -1021,8 +1019,10 @@ Some other content
list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]), list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]),
list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]), list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]),
list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]), list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]),
list_item(143..154, 2, Ordered(2), vec![p("Goodbyte", 146..154)]), list_item(143..159, 2, Ordered(2), vec![p("Goodbyte", 146..154)]),
list_item(155..161, 1, Unordered, vec![p("Last", 157..161)]), list_item(160..180, 3, Unordered, vec![p("Next item empty", 165..180)]),
list_item(186..190, 3, Unordered, vec![]),
list_item(191..197, 1, Unordered, vec![p("Last", 193..197)]),
] ]
); );
} }

View File

@@ -1778,7 +1778,7 @@ impl MultiBuffer {
&self, &self,
point: T, point: T,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> Cow<'a, LanguageSettings> {
let mut language = None; let mut language = None;
let mut file = None; let mut file = None;
if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) { if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) {
@@ -1786,7 +1786,7 @@ impl MultiBuffer {
language = buffer.language_at(offset); language = buffer.language_at(offset);
file = buffer.file(); file = buffer.file();
} }
language_settings(language.as_ref(), file, cx) language_settings(language.map(|l| l.name()), file, cx)
} }
pub fn for_each_buffer(&self, mut f: impl FnMut(&Model<Buffer>)) { pub fn for_each_buffer(&self, mut f: impl FnMut(&Model<Buffer>)) {
@@ -3580,14 +3580,14 @@ impl MultiBufferSnapshot {
&'a self, &'a self,
point: T, point: T,
cx: &'a AppContext, cx: &'a AppContext,
) -> &'a LanguageSettings { ) -> Cow<'a, LanguageSettings> {
let mut language = None; let mut language = None;
let mut file = None; let mut file = None;
if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { if let Some((buffer, offset)) = self.point_to_buffer_offset(point) {
language = buffer.language_at(offset); language = buffer.language_at(offset);
file = buffer.file(); file = buffer.file();
} }
language_settings(language, file, cx) language_settings(language.map(|l| l.name()), file, cx)
} }
pub fn language_scope_at<T: ToOffset>(&self, point: T) -> Option<LanguageScope> { pub fn language_scope_at<T: ToOffset>(&self, point: T) -> Option<LanguageScope> {

View File

@@ -293,3 +293,6 @@ pub fn local_tasks_file_relative_path() -> &'static Path {
pub fn local_vscode_tasks_file_relative_path() -> &'static Path { pub fn local_vscode_tasks_file_relative_path() -> &'static Path {
Path::new(".vscode/tasks.json") Path::new(".vscode/tasks.json")
} }
/// A default editorconfig file name to use when resolving project settings.
pub const EDITORCONFIG_NAME: &str = ".editorconfig";

View File

@@ -205,7 +205,7 @@ impl Prettier {
let params = buffer let params = buffer
.update(cx, |buffer, cx| { .update(cx, |buffer, cx| {
let buffer_language = buffer.language(); let buffer_language = buffer.language();
let language_settings = language_settings(buffer_language, buffer.file(), cx); let language_settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx);
let prettier_settings = &language_settings.prettier; let prettier_settings = &language_settings.prettier;
anyhow::ensure!( anyhow::ensure!(
prettier_settings.allowed, prettier_settings.allowed,

View File

@@ -2303,7 +2303,9 @@ impl LspCommand for OnTypeFormatting {
.await?; .await?;
let options = buffer.update(&mut cx, |buffer, cx| { let options = buffer.update(&mut cx, |buffer, cx| {
lsp_formatting_options(language_settings(buffer.language(), buffer.file(), cx)) lsp_formatting_options(
language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).as_ref(),
)
})?; })?;
Ok(Self { Ok(Self {

View File

@@ -30,8 +30,7 @@ use gpui::{
use http_client::HttpClient; use http_client::HttpClient;
use language::{ use language::{
language_settings::{ language_settings::{
all_language_settings, language_settings, AllLanguageSettings, FormatOnSave, Formatter, language_settings, FormatOnSave, Formatter, LanguageSettings, SelectedFormatter,
LanguageSettings, SelectedFormatter,
}, },
markdown, point_to_lsp, prepare_completion_documentation, markdown, point_to_lsp, prepare_completion_documentation,
proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version},
@@ -223,7 +222,8 @@ impl LocalLspStore {
})?; })?;
let settings = buffer.handle.update(&mut cx, |buffer, cx| { let settings = buffer.handle.update(&mut cx, |buffer, cx| {
language_settings(buffer.language(), buffer.file(), cx).clone() language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
.into_owned()
})?; })?;
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
@@ -280,7 +280,7 @@ impl LocalLspStore {
.zip(buffer.abs_path.as_ref()); .zip(buffer.abs_path.as_ref());
let prettier_settings = buffer.handle.read_with(&cx, |buffer, cx| { let prettier_settings = buffer.handle.read_with(&cx, |buffer, cx| {
language_settings(buffer.language(), buffer.file(), cx) language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx)
.prettier .prettier
.clone() .clone()
})?; })?;
@@ -1225,7 +1225,8 @@ impl LspStore {
}); });
let buffer_file = buffer.read(cx).file().cloned(); let buffer_file = buffer.read(cx).file().cloned();
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone(); let settings =
language_settings(Some(new_language.name()), buffer_file.as_ref(), cx).into_owned();
let buffer_file = File::from_dyn(buffer_file.as_ref()); let buffer_file = File::from_dyn(buffer_file.as_ref());
let worktree_id = if let Some(file) = buffer_file { let worktree_id = if let Some(file) = buffer_file {
@@ -1400,15 +1401,17 @@ impl LspStore {
let buffer = buffer.read(cx); let buffer = buffer.read(cx);
let buffer_file = File::from_dyn(buffer.file()); let buffer_file = File::from_dyn(buffer.file());
let buffer_language = buffer.language(); let buffer_language = buffer.language();
let settings = language_settings(buffer_language, buffer.file(), cx); let settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx);
if let Some(language) = buffer_language { if let Some(language) = buffer_language {
if settings.enable_language_server { if settings.enable_language_server {
if let Some(file) = buffer_file { if let Some(file) = buffer_file {
language_servers_to_start.push((file.worktree.clone(), language.name())); language_servers_to_start.push((file.worktree.clone(), language.name()));
} }
} }
language_formatters_to_check language_formatters_to_check.push((
.push((buffer_file.map(|f| f.worktree_id(cx)), settings.clone())); buffer_file.map(|f| f.worktree_id(cx)),
settings.into_owned(),
));
} }
} }
@@ -1433,10 +1436,13 @@ impl LspStore {
}); });
if let Some((language, adapter)) = language { if let Some((language, adapter)) = language {
let worktree = self.worktree_for_id(worktree_id, cx).ok(); let worktree = self.worktree_for_id(worktree_id, cx).ok();
let file = worktree.as_ref().and_then(|tree| { let root_file = worktree.as_ref().and_then(|worktree| {
tree.update(cx, |tree, cx| tree.root_file(cx).map(|f| f as _)) worktree
.update(cx, |tree, cx| tree.root_file(cx))
.map(|f| f as _)
}); });
if !language_settings(Some(language), file.as_ref(), cx).enable_language_server { let settings = language_settings(Some(language.name()), root_file.as_ref(), cx);
if !settings.enable_language_server {
language_servers_to_stop.push((worktree_id, started_lsp_name.clone())); language_servers_to_stop.push((worktree_id, started_lsp_name.clone()));
} else if let Some(worktree) = worktree { } else if let Some(worktree) = worktree {
let server_name = &adapter.name; let server_name = &adapter.name;
@@ -1753,10 +1759,9 @@ impl LspStore {
}) })
.filter(|_| { .filter(|_| {
maybe!({ maybe!({
let language_name = buffer.read(cx).language_at(position)?.name(); let language = buffer.read(cx).language_at(position)?;
Some( Some(
AllLanguageSettings::get_global(cx) language_settings(Some(language.name()), buffer.read(cx).file(), cx)
.language(Some(&language_name))
.linked_edits, .linked_edits,
) )
}) == Some(true) }) == Some(true)
@@ -1850,11 +1855,14 @@ impl LspStore {
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Task<Result<Option<Transaction>>> { ) -> Task<Result<Option<Transaction>>> {
let options = buffer.update(cx, |buffer, cx| { let options = buffer.update(cx, |buffer, cx| {
lsp_command::lsp_formatting_options(language_settings( lsp_command::lsp_formatting_options(
buffer.language_at(position).as_ref(), language_settings(
buffer.file(), buffer.language_at(position).map(|l| l.name()),
cx, buffer.file(),
)) cx,
)
.as_ref(),
)
}); });
self.request_lsp( self.request_lsp(
buffer.clone(), buffer.clone(),
@@ -5288,23 +5296,16 @@ impl LspStore {
}) })
} }
fn language_settings<'a>(
&'a self,
worktree: &'a Model<Worktree>,
language: &LanguageName,
cx: &'a mut ModelContext<Self>,
) -> &'a LanguageSettings {
let root_file = worktree.update(cx, |tree, cx| tree.root_file(cx));
all_language_settings(root_file.map(|f| f as _).as_ref(), cx).language(Some(language))
}
pub fn start_language_servers( pub fn start_language_servers(
&mut self, &mut self,
worktree: &Model<Worktree>, worktree: &Model<Worktree>,
language: LanguageName, language: LanguageName,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) { ) {
let settings = self.language_settings(worktree, &language, cx); let root_file = worktree
.update(cx, |tree, cx| tree.root_file(cx))
.map(|f| f as _);
let settings = language_settings(Some(language.clone()), root_file.as_ref(), cx);
if !settings.enable_language_server || self.mode.is_remote() { if !settings.enable_language_server || self.mode.is_remote() {
return; return;
} }

View File

@@ -1243,6 +1243,10 @@ impl Project {
self.client.clone() self.client.clone()
} }
pub fn ssh_client(&self) -> Option<Model<SshRemoteClient>> {
self.ssh_client.clone()
}
pub fn user_store(&self) -> Model<UserStore> { pub fn user_store(&self) -> Model<UserStore> {
self.user_store.clone() self.user_store.clone()
} }

View File

@@ -5,7 +5,7 @@ use gpui::{AppContext, AsyncAppContext, BorrowAppContext, EventEmitter, Model, M
use language::LanguageServerName; use language::LanguageServerName;
use paths::{ use paths::{
local_settings_file_relative_path, local_tasks_file_relative_path, local_settings_file_relative_path, local_tasks_file_relative_path,
local_vscode_tasks_file_relative_path, local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
}; };
use rpc::{proto, AnyProtoClient, TypedEnvelope}; use rpc::{proto, AnyProtoClient, TypedEnvelope};
use schemars::JsonSchema; use schemars::JsonSchema;
@@ -287,14 +287,29 @@ impl SettingsObserver {
let store = cx.global::<SettingsStore>(); let store = cx.global::<SettingsStore>();
for worktree in self.worktree_store.read(cx).worktrees() { for worktree in self.worktree_store.read(cx).worktrees() {
let worktree_id = worktree.read(cx).id().to_proto(); let worktree_id = worktree.read(cx).id().to_proto();
for (path, kind, content) in store.local_settings(worktree.read(cx).id()) { for (path, content) in store.local_settings(worktree.read(cx).id()) {
downstream_client downstream_client
.send(proto::UpdateWorktreeSettings { .send(proto::UpdateWorktreeSettings {
project_id, project_id,
worktree_id, worktree_id,
path: path.to_string_lossy().into(), path: path.to_string_lossy().into(),
content: Some(content), content: Some(content),
kind: Some(local_settings_kind_to_proto(kind).into()), kind: Some(
local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
),
})
.log_err();
}
for (path, content, _) in store.local_editorconfig_settings(worktree.read(cx).id()) {
downstream_client
.send(proto::UpdateWorktreeSettings {
project_id,
worktree_id,
path: path.to_string_lossy().into(),
content: Some(content),
kind: Some(
local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
),
}) })
.log_err(); .log_err();
} }
@@ -453,6 +468,11 @@ impl SettingsObserver {
.unwrap(), .unwrap(),
); );
(settings_dir, LocalSettingsKind::Tasks) (settings_dir, LocalSettingsKind::Tasks)
} else if path.ends_with(EDITORCONFIG_NAME) {
let Some(settings_dir) = path.parent().map(Arc::from) else {
continue;
};
(settings_dir, LocalSettingsKind::Editorconfig)
} else { } else {
continue; continue;
}; };

View File

@@ -4,7 +4,9 @@ use futures::{future, StreamExt};
use gpui::{AppContext, SemanticVersion, UpdateGlobal}; use gpui::{AppContext, SemanticVersion, UpdateGlobal};
use http_client::Url; use http_client::Url;
use language::{ use language::{
language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent}, language_settings::{
language_settings, AllLanguageSettings, LanguageSettingsContent, SoftWrap,
},
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter, tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter,
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
}; };
@@ -15,7 +17,7 @@ use serde_json::json;
#[cfg(not(windows))] #[cfg(not(windows))]
use std::os; use std::os;
use std::{mem, ops::Range, task::Poll}; use std::{mem, num::NonZeroU32, ops::Range, task::Poll};
use task::{ResolvedTask, TaskContext}; use task::{ResolvedTask, TaskContext};
use unindent::Unindent as _; use unindent::Unindent as _;
use util::{assert_set_eq, paths::PathMatcher, test::temp_tree, TryFutureExt as _}; use util::{assert_set_eq, paths::PathMatcher, test::temp_tree, TryFutureExt as _};
@@ -91,6 +93,107 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
}); });
} }
#[gpui::test]
async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
init_test(cx);
let dir = temp_tree(json!({
".editorconfig": r#"
root = true
[*.rs]
indent_style = tab
indent_size = 3
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 80
[*.js]
tab_width = 10
"#,
".zed": {
"settings.json": r#"{
"tab_size": 8,
"hard_tabs": false,
"ensure_final_newline_on_save": false,
"remove_trailing_whitespace_on_save": false,
"preferred_line_length": 64,
"soft_wrap": "editor_width"
}"#,
},
"a.rs": "fn a() {\n A\n}",
"b": {
".editorconfig": r#"
[*.rs]
indent_size = 2
max_line_length = off
"#,
"b.rs": "fn b() {\n B\n}",
},
"c.js": "def c\n C\nend",
"README.json": "tabs are better\n",
}));
let path = dir.path();
let fs = FakeFs::new(cx.executor());
fs.insert_tree_from_real_fs(path, path).await;
let project = Project::test(fs, [path], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
language_registry.add(js_lang());
language_registry.add(json_lang());
language_registry.add(rust_lang());
let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
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(path).unwrap().clone();
let file = File::for_entry(file_entry, worktree.clone());
let file_language = project
.read(cx)
.languages()
.language_for_file_path(file.path.as_ref());
let file_language = cx
.background_executor()
.block(file_language)
.expect("Failed to get file language");
let file = file as _;
language_settings(Some(file_language.name()), Some(&file), cx).into_owned()
};
let settings_a = settings_for("a.rs");
let settings_b = settings_for("b/b.rs");
let settings_c = settings_for("c.js");
let settings_readme = settings_for("README.json");
// .editorconfig overrides .zed/settings
assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3));
assert_eq!(settings_a.hard_tabs, true);
assert_eq!(settings_a.ensure_final_newline_on_save, true);
assert_eq!(settings_a.remove_trailing_whitespace_on_save, true);
assert_eq!(settings_a.preferred_line_length, 80);
// "max_line_length" also sets "soft_wrap"
assert_eq!(settings_a.soft_wrap, SoftWrap::PreferredLineLength);
// .editorconfig in b/ overrides .editorconfig in root
assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2));
// "indent_size" is not set, so "tab_width" is used
assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10));
// When max_line_length is "off", default to .zed/settings.json
assert_eq!(settings_b.preferred_line_length, 64);
assert_eq!(settings_b.soft_wrap, SoftWrap::EditorWidth);
// README.md should not be affected by .editorconfig's globe "*.rs"
assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8));
});
}
#[gpui::test] #[gpui::test]
async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) { async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
init_test(cx); init_test(cx);
@@ -146,26 +249,16 @@ async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext)
.update(|cx| { .update(|cx| {
let tree = worktree.read(cx); let tree = worktree.read(cx);
let settings_a = language_settings( let file_a = File::for_entry(
None, tree.entry_for_path("a/a.rs").unwrap().clone(),
Some( worktree.clone(),
&(File::for_entry( ) as _;
tree.entry_for_path("a/a.rs").unwrap().clone(), let settings_a = language_settings(None, Some(&file_a), cx);
worktree.clone(), let file_b = File::for_entry(
) as _), tree.entry_for_path("b/b.rs").unwrap().clone(),
), worktree.clone(),
cx, ) as _;
); let settings_b = language_settings(None, Some(&file_b), cx);
let settings_b = language_settings(
None,
Some(
&(File::for_entry(
tree.entry_for_path("b/b.rs").unwrap().clone(),
worktree.clone(),
) as _),
),
cx,
);
assert_eq!(settings_a.tab_size.get(), 8); assert_eq!(settings_a.tab_size.get(), 8);
assert_eq!(settings_b.tab_size.get(), 2); assert_eq!(settings_b.tab_size.get(), 2);

View File

@@ -91,7 +91,6 @@ struct EditState {
entry_id: ProjectEntryId, entry_id: ProjectEntryId,
is_new_entry: bool, is_new_entry: bool,
is_dir: bool, is_dir: bool,
is_symlink: bool,
depth: usize, depth: usize,
processing_filename: Option<String>, processing_filename: Option<String>,
} }
@@ -987,7 +986,6 @@ impl ProjectPanel {
is_new_entry: true, is_new_entry: true,
is_dir, is_dir,
processing_filename: None, processing_filename: None,
is_symlink: false,
depth: 0, depth: 0,
}); });
self.filename_editor.update(cx, |editor, cx| { self.filename_editor.update(cx, |editor, cx| {
@@ -1027,7 +1025,6 @@ impl ProjectPanel {
is_new_entry: false, is_new_entry: false,
is_dir: entry.is_dir(), is_dir: entry.is_dir(),
processing_filename: None, processing_filename: None,
is_symlink: entry.is_symlink,
depth: 0, depth: 0,
}); });
let file_name = entry let file_name = entry
@@ -1533,16 +1530,15 @@ impl ProjectPanel {
fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) { fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
if let Some((worktree, entry)) = self.selected_sub_entry(cx) { if let Some((worktree, entry)) = self.selected_sub_entry(cx) {
let abs_path = worktree.abs_path().join(&entry.path); let abs_path = match &entry.canonical_path {
Some(canonical_path) => Some(canonical_path.to_path_buf()),
None => worktree.absolutize(&entry.path).ok(),
};
let working_directory = if entry.is_dir() { let working_directory = if entry.is_dir() {
Some(abs_path) abs_path
} else { } else {
if entry.is_symlink { abs_path.and_then(|path| Some(path.parent()?.to_path_buf()))
abs_path.canonicalize().ok()
} else {
Some(abs_path)
}
.and_then(|path| Some(path.parent()?.to_path_buf()))
}; };
if let Some(working_directory) = working_directory { if let Some(working_directory) = working_directory {
cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone()) cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
@@ -1830,7 +1826,6 @@ impl ProjectPanel {
.unwrap_or_default(); .unwrap_or_default();
if let Some(edit_state) = &mut self.edit_state { if let Some(edit_state) = &mut self.edit_state {
if edit_state.entry_id == entry.id { if edit_state.entry_id == entry.id {
edit_state.is_symlink = entry.is_symlink;
edit_state.depth = depth; edit_state.depth = depth;
} }
} }
@@ -1861,7 +1856,6 @@ impl ProjectPanel {
is_private: false, is_private: false,
git_status: entry.git_status, git_status: entry.git_status,
canonical_path: entry.canonical_path.clone(), canonical_path: entry.canonical_path.clone(),
is_symlink: entry.is_symlink,
char_bag: entry.char_bag, char_bag: entry.char_bag,
is_fifo: entry.is_fifo, is_fifo: entry.is_fifo,
}); });
@@ -1920,7 +1914,7 @@ impl ProjectPanel {
let width_estimate = item_width_estimate( let width_estimate = item_width_estimate(
depth, depth,
path.to_string_lossy().chars().count(), path.to_string_lossy().chars().count(),
entry.is_symlink, entry.canonical_path.is_some(),
); );
match max_width_item.as_mut() { match max_width_item.as_mut() {

View File

@@ -12,6 +12,7 @@ message Envelope {
uint32 id = 1; uint32 id = 1;
optional uint32 responding_to = 2; optional uint32 responding_to = 2;
optional PeerId original_sender_id = 3; optional PeerId original_sender_id = 3;
optional uint32 ack_id = 266;
oneof payload { oneof payload {
Hello hello = 4; Hello hello = 4;
@@ -295,7 +296,9 @@ message Envelope {
OpenServerSettings open_server_settings = 263; OpenServerSettings open_server_settings = 263;
GetPermalinkToLine get_permalink_to_line = 264; GetPermalinkToLine get_permalink_to_line = 264;
GetPermalinkToLineResponse get_permalink_to_line_response = 265; // current max GetPermalinkToLineResponse get_permalink_to_line_response = 265;
FlushBufferedMessages flush_buffered_messages = 267;
} }
reserved 87 to 88; reserved 87 to 88;
@@ -1867,12 +1870,13 @@ message Entry {
string path = 3; string path = 3;
uint64 inode = 4; uint64 inode = 4;
Timestamp mtime = 5; Timestamp mtime = 5;
bool is_symlink = 6;
bool is_ignored = 7; bool is_ignored = 7;
bool is_external = 8; bool is_external = 8;
reserved 6;
optional GitStatus git_status = 9; optional GitStatus git_status = 9;
bool is_fifo = 10; bool is_fifo = 10;
optional uint64 size = 11; optional uint64 size = 11;
optional string canonical_path = 12;
} }
message RepositoryEntry { message RepositoryEntry {
@@ -2521,3 +2525,6 @@ message GetPermalinkToLine {
message GetPermalinkToLineResponse { message GetPermalinkToLineResponse {
string permalink = 1; string permalink = 1;
} }
message FlushBufferedMessages {}
message FlushBufferedMessagesResponse {}

View File

@@ -32,6 +32,7 @@ macro_rules! messages {
responding_to, responding_to,
original_sender_id, original_sender_id,
payload: Some(envelope::Payload::$name(self)), payload: Some(envelope::Payload::$name(self)),
ack_id: None,
} }
} }

View File

@@ -372,6 +372,7 @@ messages!(
(OpenServerSettings, Foreground), (OpenServerSettings, Foreground),
(GetPermalinkToLine, Foreground), (GetPermalinkToLine, Foreground),
(GetPermalinkToLineResponse, Foreground), (GetPermalinkToLineResponse, Foreground),
(FlushBufferedMessages, Foreground),
); );
request_messages!( request_messages!(
@@ -498,6 +499,7 @@ request_messages!(
(RemoveWorktree, Ack), (RemoveWorktree, Ack),
(OpenServerSettings, OpenBufferResponse), (OpenServerSettings, OpenBufferResponse),
(GetPermalinkToLine, GetPermalinkToLineResponse), (GetPermalinkToLine, GetPermalinkToLineResponse),
(FlushBufferedMessages, Ack),
); );
entity_messages!( entity_messages!(

View File

@@ -19,6 +19,7 @@ test-support = ["fs/test-support"]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true collections.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true

View File

@@ -6,6 +6,7 @@ use crate::{
proxy::ProxyLaunchError, proxy::ProxyLaunchError,
}; };
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use async_trait::async_trait;
use collections::HashMap; use collections::HashMap;
use futures::{ use futures::{
channel::{ channel::{
@@ -13,7 +14,7 @@ use futures::{
oneshot, oneshot,
}, },
future::BoxFuture, future::BoxFuture,
select_biased, AsyncReadExt as _, Future, FutureExt as _, SinkExt, StreamExt as _, select_biased, AsyncReadExt as _, Future, FutureExt as _, StreamExt as _,
}; };
use gpui::{ use gpui::{
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SemanticVersion, Task, AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, SemanticVersion, Task,
@@ -30,13 +31,14 @@ use smol::{
}; };
use std::{ use std::{
any::TypeId, any::TypeId,
collections::VecDeque,
ffi::OsStr, ffi::OsStr,
fmt, fmt,
ops::ControlFlow, ops::ControlFlow,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{ sync::{
atomic::{AtomicU32, Ordering::SeqCst}, atomic::{AtomicU32, Ordering::SeqCst},
Arc, Arc, Weak,
}, },
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@@ -275,68 +277,6 @@ async fn run_cmd(command: &mut process::Command) -> Result<String> {
} }
} }
struct ChannelForwarder {
quit_tx: UnboundedSender<()>,
forwarding_task: Task<(UnboundedSender<Envelope>, UnboundedReceiver<Envelope>)>,
}
impl ChannelForwarder {
fn new(
mut incoming_tx: UnboundedSender<Envelope>,
mut outgoing_rx: UnboundedReceiver<Envelope>,
cx: &AsyncAppContext,
) -> (Self, UnboundedSender<Envelope>, UnboundedReceiver<Envelope>) {
let (quit_tx, mut quit_rx) = mpsc::unbounded::<()>();
let (proxy_incoming_tx, mut proxy_incoming_rx) = mpsc::unbounded::<Envelope>();
let (mut proxy_outgoing_tx, proxy_outgoing_rx) = mpsc::unbounded::<Envelope>();
let forwarding_task = cx.background_executor().spawn(async move {
loop {
select_biased! {
_ = quit_rx.next().fuse() => {
break;
},
incoming_envelope = proxy_incoming_rx.next().fuse() => {
if let Some(envelope) = incoming_envelope {
if incoming_tx.send(envelope).await.is_err() {
break;
}
} else {
break;
}
}
outgoing_envelope = outgoing_rx.next().fuse() => {
if let Some(envelope) = outgoing_envelope {
if proxy_outgoing_tx.send(envelope).await.is_err() {
break;
}
} else {
break;
}
}
}
}
(incoming_tx, outgoing_rx)
});
(
Self {
forwarding_task,
quit_tx,
},
proxy_incoming_tx,
proxy_outgoing_rx,
)
}
async fn into_channels(mut self) -> (UnboundedSender<Envelope>, UnboundedReceiver<Envelope>) {
let _ = self.quit_tx.send(()).await;
self.forwarding_task.await
}
}
const MAX_MISSED_HEARTBEATS: usize = 5; const MAX_MISSED_HEARTBEATS: usize = 5;
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5); const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5);
@@ -346,9 +286,8 @@ const MAX_RECONNECT_ATTEMPTS: usize = 3;
enum State { enum State {
Connecting, Connecting,
Connected { Connected {
ssh_connection: SshRemoteConnection, ssh_connection: Box<dyn SshRemoteProcess>,
delegate: Arc<dyn SshClientDelegate>, delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
multiplex_task: Task<Result<()>>, multiplex_task: Task<Result<()>>,
heartbeat_task: Task<Result<()>>, heartbeat_task: Task<Result<()>>,
@@ -356,18 +295,16 @@ enum State {
HeartbeatMissed { HeartbeatMissed {
missed_heartbeats: usize, missed_heartbeats: usize,
ssh_connection: SshRemoteConnection, ssh_connection: Box<dyn SshRemoteProcess>,
delegate: Arc<dyn SshClientDelegate>, delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
multiplex_task: Task<Result<()>>, multiplex_task: Task<Result<()>>,
heartbeat_task: Task<Result<()>>, heartbeat_task: Task<Result<()>>,
}, },
Reconnecting, Reconnecting,
ReconnectFailed { ReconnectFailed {
ssh_connection: SshRemoteConnection, ssh_connection: Box<dyn SshRemoteProcess>,
delegate: Arc<dyn SshClientDelegate>, delegate: Arc<dyn SshClientDelegate>,
forwarder: ChannelForwarder,
error: anyhow::Error, error: anyhow::Error,
attempts: usize, attempts: usize,
@@ -391,11 +328,11 @@ impl fmt::Display for State {
} }
impl State { impl State {
fn ssh_connection(&self) -> Option<&SshRemoteConnection> { fn ssh_connection(&self) -> Option<&dyn SshRemoteProcess> {
match self { match self {
Self::Connected { ssh_connection, .. } => Some(ssh_connection), Self::Connected { ssh_connection, .. } => Some(ssh_connection.as_ref()),
Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection), Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection), Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.as_ref()),
_ => None, _ => None,
} }
} }
@@ -429,14 +366,12 @@ impl State {
Self::HeartbeatMissed { Self::HeartbeatMissed {
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
multiplex_task, multiplex_task,
heartbeat_task, heartbeat_task,
.. ..
} => Self::Connected { } => Self::Connected {
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
multiplex_task, multiplex_task,
heartbeat_task, heartbeat_task,
}, },
@@ -449,14 +384,12 @@ impl State {
Self::Connected { Self::Connected {
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
multiplex_task, multiplex_task,
heartbeat_task, heartbeat_task,
} => Self::HeartbeatMissed { } => Self::HeartbeatMissed {
missed_heartbeats: 1, missed_heartbeats: 1,
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
multiplex_task, multiplex_task,
heartbeat_task, heartbeat_task,
}, },
@@ -464,14 +397,12 @@ impl State {
missed_heartbeats, missed_heartbeats,
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
multiplex_task, multiplex_task,
heartbeat_task, heartbeat_task,
} => Self::HeartbeatMissed { } => Self::HeartbeatMissed {
missed_heartbeats: missed_heartbeats + 1, missed_heartbeats: missed_heartbeats + 1,
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
multiplex_task, multiplex_task,
heartbeat_task, heartbeat_task,
}, },
@@ -529,7 +460,8 @@ impl SshRemoteClient {
let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>(); let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
let client = cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx))?; let client =
cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "client"))?;
let this = cx.new_model(|_| Self { let this = cx.new_model(|_| Self {
client: client.clone(), client: client.clone(),
unique_identifier: unique_identifier.clone(), unique_identifier: unique_identifier.clone(),
@@ -537,26 +469,19 @@ impl SshRemoteClient {
state: Arc::new(Mutex::new(Some(State::Connecting))), state: Arc::new(Mutex::new(Some(State::Connecting))),
})?; })?;
let (proxy, proxy_incoming_tx, proxy_outgoing_rx) = let (ssh_connection, io_task) = Self::establish_connection(
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
let (ssh_connection, ssh_proxy_process) = Self::establish_connection(
unique_identifier, unique_identifier,
false, false,
connection_options, connection_options,
incoming_tx,
outgoing_rx,
connection_activity_tx,
delegate.clone(), delegate.clone(),
&mut cx, &mut cx,
) )
.await?; .await?;
let multiplex_task = Self::multiplex( let multiplex_task = Self::monitor(this.downgrade(), io_task, &cx);
this.downgrade(),
ssh_proxy_process,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
&mut cx,
);
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await { if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await {
log::error!("failed to establish connection: {}", error); log::error!("failed to establish connection: {}", error);
@@ -570,7 +495,6 @@ impl SshRemoteClient {
*this.state.lock() = Some(State::Connected { *this.state.lock() = Some(State::Connected {
ssh_connection, ssh_connection,
delegate, delegate,
forwarder: proxy,
multiplex_task, multiplex_task,
heartbeat_task, heartbeat_task,
}); });
@@ -592,7 +516,6 @@ impl SshRemoteClient {
heartbeat_task, heartbeat_task,
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
} = state } = state
else { else {
return None; return None;
@@ -616,7 +539,6 @@ impl SshRemoteClient {
drop(heartbeat_task); drop(heartbeat_task);
drop(ssh_connection); drop(ssh_connection);
drop(delegate); drop(delegate);
drop(forwarder);
}) })
} }
@@ -638,33 +560,30 @@ impl SshRemoteClient {
} }
let state = lock.take().unwrap(); let state = lock.take().unwrap();
let (attempts, mut ssh_connection, delegate, forwarder) = match state { let (attempts, mut ssh_connection, delegate) = match state {
State::Connected { State::Connected {
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
multiplex_task, multiplex_task,
heartbeat_task, heartbeat_task,
} }
| State::HeartbeatMissed { | State::HeartbeatMissed {
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
multiplex_task, multiplex_task,
heartbeat_task, heartbeat_task,
.. ..
} => { } => {
drop(multiplex_task); drop(multiplex_task);
drop(heartbeat_task); drop(heartbeat_task);
(0, ssh_connection, delegate, forwarder) (0, ssh_connection, delegate)
} }
State::ReconnectFailed { State::ReconnectFailed {
attempts, attempts,
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
.. ..
} => (attempts, ssh_connection, delegate, forwarder), } => (attempts, ssh_connection, delegate),
State::Connecting State::Connecting
| State::Reconnecting | State::Reconnecting
| State::ReconnectExhausted | State::ReconnectExhausted
@@ -691,41 +610,37 @@ impl SshRemoteClient {
let client = self.client.clone(); let client = self.client.clone();
let reconnect_task = cx.spawn(|this, mut cx| async move { let reconnect_task = cx.spawn(|this, mut cx| async move {
macro_rules! failed { macro_rules! failed {
($error:expr, $attempts:expr, $ssh_connection:expr, $delegate:expr, $forwarder:expr) => { ($error:expr, $attempts:expr, $ssh_connection:expr, $delegate:expr) => {
return State::ReconnectFailed { return State::ReconnectFailed {
error: anyhow!($error), error: anyhow!($error),
attempts: $attempts, attempts: $attempts,
ssh_connection: $ssh_connection, ssh_connection: $ssh_connection,
delegate: $delegate, delegate: $delegate,
forwarder: $forwarder,
}; };
}; };
} }
if let Err(error) = ssh_connection.master_process.kill() {
failed!(error, attempts, ssh_connection, delegate, forwarder);
};
if let Err(error) = ssh_connection if let Err(error) = ssh_connection
.master_process .kill()
.status()
.await .await
.context("Failed to kill ssh process") .context("Failed to kill ssh process")
{ {
failed!(error, attempts, ssh_connection, delegate, forwarder); failed!(error, attempts, ssh_connection, delegate);
} };
let connection_options = ssh_connection.socket.connection_options.clone(); let connection_options = ssh_connection.connection_options();
let (incoming_tx, outgoing_rx) = forwarder.into_channels().await; let (outgoing_tx, outgoing_rx) = mpsc::unbounded::<Envelope>();
let (forwarder, proxy_incoming_tx, proxy_outgoing_rx) = let (incoming_tx, incoming_rx) = mpsc::unbounded::<Envelope>();
ChannelForwarder::new(incoming_tx, outgoing_rx, &mut cx);
let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1);
let (ssh_connection, ssh_process) = match Self::establish_connection( let (ssh_connection, io_task) = match Self::establish_connection(
identifier, identifier,
true, true,
connection_options, connection_options,
incoming_tx,
outgoing_rx,
connection_activity_tx,
delegate.clone(), delegate.clone(),
&mut cx, &mut cx,
) )
@@ -733,27 +648,20 @@ impl SshRemoteClient {
{ {
Ok((ssh_connection, ssh_process)) => (ssh_connection, ssh_process), Ok((ssh_connection, ssh_process)) => (ssh_connection, ssh_process),
Err(error) => { Err(error) => {
failed!(error, attempts, ssh_connection, delegate, forwarder); failed!(error, attempts, ssh_connection, delegate);
} }
}; };
let multiplex_task = Self::multiplex( let multiplex_task = Self::monitor(this.clone(), io_task, &cx);
this.clone(), client.reconnect(incoming_rx, outgoing_tx, &cx);
ssh_process,
proxy_incoming_tx,
proxy_outgoing_rx,
connection_activity_tx,
&mut cx,
);
if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await { if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await {
failed!(error, attempts, ssh_connection, delegate, forwarder); failed!(error, attempts, ssh_connection, delegate);
}; };
State::Connected { State::Connected {
ssh_connection, ssh_connection,
delegate, delegate,
forwarder,
multiplex_task, multiplex_task,
heartbeat_task: Self::heartbeat(this.clone(), connection_activity_rx, &mut cx), heartbeat_task: Self::heartbeat(this.clone(), connection_activity_rx, &mut cx),
} }
@@ -797,7 +705,7 @@ impl SshRemoteClient {
cx.emit(SshRemoteEvent::Disconnected); cx.emit(SshRemoteEvent::Disconnected);
Ok(()) Ok(())
} else { } else {
log::debug!("State has transition from Reconnecting into new state while attempting reconnect. Ignoring new state."); log::debug!("State has transition from Reconnecting into new state while attempting reconnect.");
Ok(()) Ok(())
} }
}) })
@@ -910,13 +818,12 @@ impl SshRemoteClient {
} }
fn multiplex( fn multiplex(
this: WeakModel<Self>,
mut ssh_proxy_process: Child, mut ssh_proxy_process: Child,
incoming_tx: UnboundedSender<Envelope>, incoming_tx: UnboundedSender<Envelope>,
mut outgoing_rx: UnboundedReceiver<Envelope>, mut outgoing_rx: UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>, mut connection_activity_tx: Sender<()>,
cx: &AsyncAppContext, cx: &AsyncAppContext,
) -> Task<Result<()>> { ) -> Task<Result<i32>> {
let mut child_stderr = ssh_proxy_process.stderr.take().unwrap(); let mut child_stderr = ssh_proxy_process.stderr.take().unwrap();
let mut child_stdout = ssh_proxy_process.stdout.take().unwrap(); let mut child_stdout = ssh_proxy_process.stdout.take().unwrap();
let mut child_stdin = ssh_proxy_process.stdin.take().unwrap(); let mut child_stdin = ssh_proxy_process.stdin.take().unwrap();
@@ -988,7 +895,7 @@ impl SshRemoteClient {
} }
}); });
cx.spawn(|mut cx| async move { cx.spawn(|_| async move {
let result = futures::select! { let result = futures::select! {
result = stdin_task.fuse() => { result = stdin_task.fuse() => {
result.context("stdin") result.context("stdin")
@@ -1002,9 +909,22 @@ impl SshRemoteClient {
}; };
match result { match result {
Ok(_) => { Ok(_) => Ok(ssh_proxy_process.status().await?.code().unwrap_or(1)),
let exit_code = ssh_proxy_process.status().await?.code().unwrap_or(1); Err(error) => Err(error),
}
})
}
fn monitor(
this: WeakModel<Self>,
io_task: Task<Result<i32>>,
cx: &AsyncAppContext,
) -> Task<Result<()>> {
cx.spawn(|mut cx| async move {
let result = io_task.await;
match result {
Ok(exit_code) => {
if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) { if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) {
match error { match error {
ProxyLaunchError::ServerNotRunning => { ProxyLaunchError::ServerNotRunning => {
@@ -1058,21 +978,40 @@ impl SshRemoteClient {
cx.notify(); cx.notify();
} }
#[allow(clippy::too_many_arguments)]
async fn establish_connection( async fn establish_connection(
unique_identifier: String, unique_identifier: String,
reconnect: bool, reconnect: bool,
connection_options: SshConnectionOptions, connection_options: SshConnectionOptions,
incoming_tx: UnboundedSender<Envelope>,
outgoing_rx: UnboundedReceiver<Envelope>,
connection_activity_tx: Sender<()>,
delegate: Arc<dyn SshClientDelegate>, delegate: Arc<dyn SshClientDelegate>,
cx: &mut AsyncAppContext, cx: &mut AsyncAppContext,
) -> Result<(SshRemoteConnection, Child)> { ) -> Result<(Box<dyn SshRemoteProcess>, Task<Result<i32>>)> {
#[cfg(any(test, feature = "test-support"))]
if let Some(fake) = fake::SshRemoteConnection::new(&connection_options) {
let io_task = fake::SshRemoteConnection::multiplex(
fake.connection_options(),
incoming_tx,
outgoing_rx,
connection_activity_tx,
cx,
)
.await;
return Ok((fake, io_task));
}
let ssh_connection = let ssh_connection =
SshRemoteConnection::new(connection_options, delegate.clone(), cx).await?; SshRemoteConnection::new(connection_options, delegate.clone(), cx).await?;
let platform = ssh_connection.query_platform().await?; let platform = ssh_connection.query_platform().await?;
let remote_binary_path = delegate.remote_server_binary_path(platform, cx)?; let remote_binary_path = delegate.remote_server_binary_path(platform, cx)?;
ssh_connection if !reconnect {
.ensure_server_binary(&delegate, &remote_binary_path, platform, cx) ssh_connection
.await?; .ensure_server_binary(&delegate, &remote_binary_path, platform, cx)
.await?;
}
let socket = ssh_connection.socket.clone(); let socket = ssh_connection.socket.clone();
run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?; run_cmd(socket.ssh_command(&remote_binary_path).arg("version")).await?;
@@ -1097,7 +1036,15 @@ impl SshRemoteClient {
.spawn() .spawn()
.context("failed to spawn remote server")?; .context("failed to spawn remote server")?;
Ok((ssh_connection, ssh_proxy_process)) let io_task = Self::multiplex(
ssh_proxy_process,
incoming_tx,
outgoing_rx,
connection_activity_tx,
&cx,
);
Ok((Box::new(ssh_connection), io_task))
} }
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) { pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
@@ -1109,7 +1056,7 @@ impl SshRemoteClient {
.lock() .lock()
.as_ref() .as_ref()
.and_then(|state| state.ssh_connection()) .and_then(|state| state.ssh_connection())
.map(|ssh_connection| ssh_connection.socket.ssh_args()) .map(|ssh_connection| ssh_connection.ssh_args())
} }
pub fn proto_client(&self) -> AnyProtoClient { pub fn proto_client(&self) -> AnyProtoClient {
@@ -1124,7 +1071,6 @@ impl SshRemoteClient {
self.connection_options.clone() self.connection_options.clone()
} }
#[cfg(not(any(test, feature = "test-support")))]
pub fn connection_state(&self) -> ConnectionState { pub fn connection_state(&self) -> ConnectionState {
self.state self.state
.lock() .lock()
@@ -1133,37 +1079,59 @@ impl SshRemoteClient {
.unwrap_or(ConnectionState::Disconnected) .unwrap_or(ConnectionState::Disconnected)
} }
#[cfg(any(test, feature = "test-support"))]
pub fn connection_state(&self) -> ConnectionState {
ConnectionState::Connected
}
pub fn is_disconnected(&self) -> bool { pub fn is_disconnected(&self) -> bool {
self.connection_state() == ConnectionState::Disconnected self.connection_state() == ConnectionState::Disconnected
} }
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub fn fake( pub fn simulate_disconnect(&self, client_cx: &mut AppContext) -> Task<()> {
let port = self.connection_options().port.unwrap();
client_cx.spawn(|cx| async move {
let (channel, server_cx) = cx
.update_global(|c: &mut fake::ServerConnections, _| c.get(port))
.unwrap();
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
channel.reconnect(incoming_rx, outgoing_tx, &server_cx);
})
}
#[cfg(any(test, feature = "test-support"))]
pub fn fake_server(
client_cx: &mut gpui::TestAppContext, client_cx: &mut gpui::TestAppContext,
server_cx: &mut gpui::TestAppContext, server_cx: &mut gpui::TestAppContext,
) -> (Model<Self>, Arc<ChannelClient>) { ) -> (u16, Arc<ChannelClient>) {
use gpui::Context; use gpui::BorrowAppContext;
let (outgoing_tx, _) = mpsc::unbounded::<Envelope>();
let (_, incoming_rx) = mpsc::unbounded::<Envelope>();
let server_client =
server_cx.update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server"));
let port = client_cx.update(|cx| {
cx.update_default_global(|c: &mut fake::ServerConnections, _| {
c.push(server_client.clone(), server_cx.to_async())
})
});
(port, server_client)
}
let (server_to_client_tx, server_to_client_rx) = mpsc::unbounded(); #[cfg(any(test, feature = "test-support"))]
let (client_to_server_tx, client_to_server_rx) = mpsc::unbounded(); pub async fn fake_client(port: u16, client_cx: &mut gpui::TestAppContext) -> Model<Self> {
client_cx
( .update(|cx| {
client_cx.update(|cx| { Self::new(
let client = ChannelClient::new(server_to_client_rx, client_to_server_tx, cx); "fake".to_string(),
cx.new_model(|_| Self { SshConnectionOptions {
client, host: "<fake>".to_string(),
unique_identifier: "fake".to_string(), port: Some(port),
connection_options: SshConnectionOptions::default(), ..Default::default()
state: Arc::new(Mutex::new(None)), },
}) Arc::new(fake::Delegate),
}), cx,
server_cx.update(|cx| ChannelClient::new(client_to_server_rx, server_to_client_tx, cx)), )
) })
.await
.unwrap()
} }
} }
@@ -1173,6 +1141,13 @@ impl From<SshRemoteClient> for AnyProtoClient {
} }
} }
#[async_trait]
trait SshRemoteProcess: Send + Sync {
async fn kill(&mut self) -> Result<()>;
fn ssh_args(&self) -> Vec<String>;
fn connection_options(&self) -> SshConnectionOptions;
}
struct SshRemoteConnection { struct SshRemoteConnection {
socket: SshSocket, socket: SshSocket,
master_process: process::Child, master_process: process::Child,
@@ -1187,6 +1162,25 @@ impl Drop for SshRemoteConnection {
} }
} }
#[async_trait]
impl SshRemoteProcess for SshRemoteConnection {
async fn kill(&mut self) -> Result<()> {
self.master_process.kill()?;
self.master_process.status().await?;
Ok(())
}
fn ssh_args(&self) -> Vec<String> {
self.socket.ssh_args()
}
fn connection_options(&self) -> SshConnectionOptions {
self.socket.connection_options.clone()
}
}
impl SshRemoteConnection { impl SshRemoteConnection {
#[cfg(not(unix))] #[cfg(not(unix))]
async fn new( async fn new(
@@ -1469,9 +1463,13 @@ type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, ones
pub struct ChannelClient { pub struct ChannelClient {
next_message_id: AtomicU32, next_message_id: AtomicU32,
outgoing_tx: mpsc::UnboundedSender<Envelope>, outgoing_tx: Mutex<mpsc::UnboundedSender<Envelope>>,
response_channels: ResponseChannels, // Lock buffer: Mutex<VecDeque<Envelope>>,
message_handlers: Mutex<ProtoMessageHandlerSet>, // Lock response_channels: ResponseChannels,
message_handlers: Mutex<ProtoMessageHandlerSet>,
max_received: AtomicU32,
name: &'static str,
task: Mutex<Task<Result<()>>>,
} }
impl ChannelClient { impl ChannelClient {
@@ -1479,32 +1477,59 @@ impl ChannelClient {
incoming_rx: mpsc::UnboundedReceiver<Envelope>, incoming_rx: mpsc::UnboundedReceiver<Envelope>,
outgoing_tx: mpsc::UnboundedSender<Envelope>, outgoing_tx: mpsc::UnboundedSender<Envelope>,
cx: &AppContext, cx: &AppContext,
name: &'static str,
) -> Arc<Self> { ) -> Arc<Self> {
let this = Arc::new(Self { Arc::new_cyclic(|this| Self {
outgoing_tx, outgoing_tx: Mutex::new(outgoing_tx),
next_message_id: AtomicU32::new(0), next_message_id: AtomicU32::new(0),
max_received: AtomicU32::new(0),
response_channels: ResponseChannels::default(), response_channels: ResponseChannels::default(),
message_handlers: Default::default(), message_handlers: Default::default(),
}); buffer: Mutex::new(VecDeque::new()),
name,
Self::start_handling_messages(this.clone(), incoming_rx, cx); task: Mutex::new(Self::start_handling_messages(
this.clone(),
this incoming_rx,
&cx.to_async(),
)),
})
} }
fn start_handling_messages( fn start_handling_messages(
this: Arc<Self>, this: Weak<Self>,
mut incoming_rx: mpsc::UnboundedReceiver<Envelope>, mut incoming_rx: mpsc::UnboundedReceiver<Envelope>,
cx: &AppContext, cx: &AsyncAppContext,
) { ) -> Task<Result<()>> {
cx.spawn(|cx| { cx.spawn(|cx| {
let this = Arc::downgrade(&this);
async move { async move {
let peer_id = PeerId { owner_id: 0, id: 0 }; let peer_id = PeerId { owner_id: 0, id: 0 };
while let Some(incoming) = incoming_rx.next().await { while let Some(incoming) = incoming_rx.next().await {
let Some(this) = this.upgrade() else { let Some(this) = this.upgrade() else {
return anyhow::Ok(()); return anyhow::Ok(());
}; };
if let Some(ack_id) = incoming.ack_id {
let mut buffer = this.buffer.lock();
while buffer.front().is_some_and(|msg| msg.id <= ack_id) {
buffer.pop_front();
}
}
if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) =
&incoming.payload
{
log::debug!("{}:ssh message received. name:FlushBufferedMessages", this.name);
{
let buffer = this.buffer.lock();
for envelope in buffer.iter() {
this.outgoing_tx.lock().unbounded_send(envelope.clone()).ok();
}
}
let mut envelope = proto::Ack{}.into_envelope(0, Some(incoming.id), None);
envelope.id = this.next_message_id.fetch_add(1, SeqCst);
this.outgoing_tx.lock().unbounded_send(envelope).ok();
continue;
}
this.max_received.store(incoming.id, SeqCst);
if let Some(request_id) = incoming.responding_to { if let Some(request_id) = incoming.responding_to {
let request_id = MessageId(request_id); let request_id = MessageId(request_id);
@@ -1526,26 +1551,37 @@ impl ChannelClient {
this.clone().into(), this.clone().into(),
cx.clone(), cx.clone(),
) { ) {
log::debug!("ssh message received. name:{type_name}"); log::debug!("{}:ssh message received. name:{type_name}", this.name);
match future.await { cx.foreground_executor().spawn(async move {
Ok(_) => { match future.await {
log::debug!("ssh message handled. name:{type_name}"); Ok(_) => {
log::debug!("{}:ssh message handled. name:{type_name}", this.name);
}
Err(error) => {
log::error!(
"{}:error handling message. type:{type_name}, error:{error}", this.name,
);
}
} }
Err(error) => { }).detach()
log::error!(
"error handling message. type:{type_name}, error:{error}",
);
}
}
} else { } else {
log::error!("unhandled ssh message name:{type_name}"); log::error!("{}:unhandled ssh message name:{type_name}", this.name);
} }
} }
} }
anyhow::Ok(()) anyhow::Ok(())
} }
}) })
.detach(); }
pub fn reconnect(
self: &Arc<Self>,
incoming_rx: UnboundedReceiver<Envelope>,
outgoing_tx: UnboundedSender<Envelope>,
cx: &AsyncAppContext,
) {
*self.outgoing_tx.lock() = outgoing_tx;
*self.task.lock() = Self::start_handling_messages(Arc::downgrade(self), incoming_rx, cx);
} }
pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) { pub fn subscribe_to_entity<E: 'static>(&self, remote_id: u64, entity: &Model<E>) {
@@ -1581,6 +1617,26 @@ impl ChannelClient {
} }
} }
pub async fn resync(&self, timeout: Duration) -> Result<()> {
smol::future::or(
async {
self.request(proto::FlushBufferedMessages {}).await?;
for envelope in self.buffer.lock().iter() {
self.outgoing_tx
.lock()
.unbounded_send(envelope.clone())
.ok();
}
Ok(())
},
async {
smol::Timer::after(timeout).await;
Err(anyhow!("Timeout detected"))
},
)
.await
}
pub async fn ping(&self, timeout: Duration) -> Result<()> { pub async fn ping(&self, timeout: Duration) -> Result<()> {
smol::future::or( smol::future::or(
async { async {
@@ -1610,7 +1666,8 @@ impl ChannelClient {
let mut response_channels_lock = self.response_channels.lock(); let mut response_channels_lock = self.response_channels.lock();
response_channels_lock.insert(MessageId(envelope.id), tx); response_channels_lock.insert(MessageId(envelope.id), tx);
drop(response_channels_lock); drop(response_channels_lock);
let result = self.outgoing_tx.unbounded_send(envelope);
let result = self.send_buffered(envelope);
async move { async move {
if let Err(error) = &result { if let Err(error) = &result {
log::error!("failed to send message: {}", error); log::error!("failed to send message: {}", error);
@@ -1627,7 +1684,15 @@ impl ChannelClient {
pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> { pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> {
envelope.id = self.next_message_id.fetch_add(1, SeqCst); envelope.id = self.next_message_id.fetch_add(1, SeqCst);
self.outgoing_tx.unbounded_send(envelope)?; self.send_buffered(envelope)
}
pub fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> {
envelope.ack_id = Some(self.max_received.load(SeqCst));
self.buffer.lock().push_back(envelope.clone());
// ignore errors on send (happen while we're reconnecting)
// assume that the global "disconnected" overlay is sufficient.
self.outgoing_tx.lock().unbounded_send(envelope).ok();
Ok(()) Ok(())
} }
} }
@@ -1657,3 +1722,148 @@ impl ProtoClient for ChannelClient {
false false
} }
} }
#[cfg(any(test, feature = "test-support"))]
mod fake {
use std::{path::PathBuf, sync::Arc};
use anyhow::Result;
use async_trait::async_trait;
use futures::{
channel::{
mpsc::{self, Sender},
oneshot,
},
select_biased, FutureExt, SinkExt, StreamExt,
};
use gpui::{AsyncAppContext, BorrowAppContext, Global, SemanticVersion, Task};
use rpc::proto::Envelope;
use super::{
ChannelClient, SshClientDelegate, SshConnectionOptions, SshPlatform, SshRemoteProcess,
};
pub(super) struct SshRemoteConnection {
connection_options: SshConnectionOptions,
}
impl SshRemoteConnection {
pub(super) fn new(
connection_options: &SshConnectionOptions,
) -> Option<Box<dyn SshRemoteProcess>> {
if connection_options.host == "<fake>" {
return Some(Box::new(Self {
connection_options: connection_options.clone(),
}));
}
return None;
}
pub(super) async fn multiplex(
connection_options: SshConnectionOptions,
mut client_incoming_tx: mpsc::UnboundedSender<Envelope>,
mut client_outgoing_rx: mpsc::UnboundedReceiver<Envelope>,
mut connection_activity_tx: Sender<()>,
cx: &mut AsyncAppContext,
) -> Task<Result<i32>> {
let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::<Envelope>();
let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::<Envelope>();
let (channel, server_cx) = cx
.update(|cx| {
cx.update_global(|conns: &mut ServerConnections, _| {
conns.get(connection_options.port.unwrap())
})
})
.unwrap();
channel.reconnect(server_incoming_rx, server_outgoing_tx, &server_cx);
// send to proxy_tx to get to the server.
// receive from
cx.background_executor().spawn(async move {
loop {
select_biased! {
server_to_client = server_outgoing_rx.next().fuse() => {
let Some(server_to_client) = server_to_client else {
return Ok(1)
};
connection_activity_tx.try_send(()).ok();
client_incoming_tx.send(server_to_client).await.ok();
}
client_to_server = client_outgoing_rx.next().fuse() => {
let Some(client_to_server) = client_to_server else {
return Ok(1)
};
server_incoming_tx.send(client_to_server).await.ok();
}
}
}
})
}
}
#[async_trait]
impl SshRemoteProcess for SshRemoteConnection {
async fn kill(&mut self) -> Result<()> {
Ok(())
}
fn ssh_args(&self) -> Vec<String> {
Vec::new()
}
fn connection_options(&self) -> SshConnectionOptions {
self.connection_options.clone()
}
}
#[derive(Default)]
pub(super) struct ServerConnections(Vec<(Arc<ChannelClient>, AsyncAppContext)>);
impl Global for ServerConnections {}
impl ServerConnections {
pub(super) fn push(&mut self, server: Arc<ChannelClient>, cx: AsyncAppContext) -> u16 {
self.0.push((server.clone(), cx));
self.0.len() as u16 - 1
}
pub(super) fn get(&mut self, port: u16) -> (Arc<ChannelClient>, AsyncAppContext) {
self.0
.get(port as usize)
.expect("no fake server for port")
.clone()
}
}
pub(super) struct Delegate;
impl SshClientDelegate for Delegate {
fn ask_password(
&self,
_: String,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<String>> {
unreachable!()
}
fn remote_server_binary_path(
&self,
_: SshPlatform,
_: &mut AsyncAppContext,
) -> Result<PathBuf> {
unreachable!()
}
fn get_server_binary(
&self,
_: SshPlatform,
_: &mut AsyncAppContext,
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>> {
unreachable!()
}
fn set_status(&self, _: Option<&str>, _: &mut AsyncAppContext) {
unreachable!()
}
fn set_error(&self, _: String, _: &mut AsyncAppContext) {
unreachable!()
}
}
}

View File

@@ -5,7 +5,7 @@ use fs::{FakeFs, Fs};
use gpui::{Context, Model, TestAppContext}; use gpui::{Context, Model, TestAppContext};
use http_client::{BlockedHttpClient, FakeHttpClient}; use http_client::{BlockedHttpClient, FakeHttpClient};
use language::{ use language::{
language_settings::{all_language_settings, AllLanguageSettings}, language_settings::{language_settings, AllLanguageSettings},
Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName, Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LanguageServerName,
LineEnding, LineEnding,
}; };
@@ -208,7 +208,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
server_cx.read(|cx| { server_cx.read(|cx| {
assert_eq!( assert_eq!(
AllLanguageSettings::get_global(cx) AllLanguageSettings::get_global(cx)
.language(Some(&"Rust".into())) .language(None, Some(&"Rust".into()), cx)
.language_servers, .language_servers,
["from-local-settings".to_string()] ["from-local-settings".to_string()]
) )
@@ -228,7 +228,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
server_cx.read(|cx| { server_cx.read(|cx| {
assert_eq!( assert_eq!(
AllLanguageSettings::get_global(cx) AllLanguageSettings::get_global(cx)
.language(Some(&"Rust".into())) .language(None, Some(&"Rust".into()), cx)
.language_servers, .language_servers,
["from-server-settings".to_string()] ["from-server-settings".to_string()]
) )
@@ -287,7 +287,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
}), }),
cx cx
) )
.language(Some(&"Rust".into())) .language(None, Some(&"Rust".into()), cx)
.language_servers, .language_servers,
["override-rust-analyzer".to_string()] ["override-rust-analyzer".to_string()]
) )
@@ -296,9 +296,7 @@ async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppCo
cx.read(|cx| { cx.read(|cx| {
let file = buffer.read(cx).file(); let file = buffer.read(cx).file();
assert_eq!( assert_eq!(
all_language_settings(file, cx) language_settings(Some("Rust".into()), file, cx).language_servers,
.language(Some(&"Rust".into()))
.language_servers,
["override-rust-analyzer".to_string()] ["override-rust-analyzer".to_string()]
) )
}); });
@@ -379,9 +377,7 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
cx.read(|cx| { cx.read(|cx| {
let file = buffer.read(cx).file(); let file = buffer.read(cx).file();
assert_eq!( assert_eq!(
all_language_settings(file, cx) language_settings(Some("Rust".into()), file, cx).language_servers,
.language(Some(&"Rust".into()))
.language_servers,
["rust-analyzer".to_string()] ["rust-analyzer".to_string()]
) )
}); });
@@ -641,6 +637,47 @@ async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut Test
}) })
} }
#[gpui::test(iterations = 20)]
async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
let (project, _headless, fs) = init_test(cx, server_cx).await;
let (worktree, _) = project
.update(cx, |project, cx| {
project.find_or_create_worktree("/code/project1", true, cx)
})
.await
.unwrap();
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
let buffer = project
.update(cx, |project, cx| {
project.open_buffer((worktree_id, Path::new("src/lib.rs")), cx)
})
.await
.unwrap();
buffer.update(cx, |buffer, cx| {
assert_eq!(buffer.text(), "fn one() -> usize { 1 }");
let ix = buffer.text().find('1').unwrap();
buffer.edit([(ix..ix + 1, "100")], None, cx);
});
let client = cx.read(|cx| project.read(cx).ssh_client().unwrap());
client
.update(cx, |client, cx| client.simulate_disconnect(cx))
.detach();
project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
.await
.unwrap();
assert_eq!(
fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(),
"fn one() -> usize { 100 }"
);
}
fn init_logger() { fn init_logger() {
if std::env::var("RUST_LOG").is_ok() { if std::env::var("RUST_LOG").is_ok() {
env_logger::try_init().ok(); env_logger::try_init().ok();
@@ -651,9 +688,9 @@ async fn init_test(
cx: &mut TestAppContext, cx: &mut TestAppContext,
server_cx: &mut TestAppContext, server_cx: &mut TestAppContext,
) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) { ) -> (Model<Project>, Model<HeadlessProject>, Arc<FakeFs>) {
let (ssh_remote_client, ssh_server_client) = SshRemoteClient::fake(cx, server_cx);
init_logger(); init_logger();
let (forwarder, ssh_server_client) = SshRemoteClient::fake_server(cx, server_cx);
let fs = FakeFs::new(server_cx.executor()); let fs = FakeFs::new(server_cx.executor());
fs.insert_tree( fs.insert_tree(
"/code", "/code",
@@ -694,8 +731,9 @@ async fn init_test(
cx, cx,
) )
}); });
let project = build_project(ssh_remote_client, cx);
let ssh = SshRemoteClient::fake_client(forwarder, cx).await;
let project = build_project(ssh, cx);
project project
.update(cx, { .update(cx, {
let headless = headless.clone(); let headless = headless.clone();

View File

@@ -279,7 +279,7 @@ fn start_server(
}) })
.detach(); .detach();
ChannelClient::new(incoming_rx, outgoing_tx, cx) ChannelClient::new(incoming_rx, outgoing_tx, cx, "server")
} }
fn init_paths() -> anyhow::Result<()> { fn init_paths() -> anyhow::Result<()> {

View File

@@ -18,6 +18,7 @@ test-support = ["gpui/test-support", "fs/test-support"]
[dependencies] [dependencies]
anyhow.workspace = true anyhow.workspace = true
collections.workspace = true collections.workspace = true
ec4rs.workspace = true
fs.workspace = true fs.workspace = true
futures.workspace = true futures.workspace = true
gpui.workspace = true gpui.workspace = true

View File

@@ -1,9 +1,10 @@
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use collections::{btree_map, hash_map, BTreeMap, HashMap}; use collections::{btree_map, hash_map, BTreeMap, HashMap};
use ec4rs::{ConfigParser, PropertiesSource, Section};
use fs::Fs; use fs::Fs;
use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt}; use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt};
use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global, Task, UpdateGlobal}; use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global, Task, UpdateGlobal};
use paths::local_settings_file_relative_path; use paths::{local_settings_file_relative_path, EDITORCONFIG_NAME};
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema}; use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
use serde::{de::DeserializeOwned, Deserialize as _, Serialize}; use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -12,12 +13,14 @@ use std::{
fmt::Debug, fmt::Debug,
ops::Range, ops::Range,
path::{Path, PathBuf}, path::{Path, PathBuf},
str, str::{self, FromStr},
sync::{Arc, LazyLock}, sync::{Arc, LazyLock},
}; };
use tree_sitter::Query; use tree_sitter::Query;
use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _}; use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
pub type EditorconfigProperties = ec4rs::Properties;
use crate::{SettingsJsonSchemaParams, WorktreeId}; use crate::{SettingsJsonSchemaParams, WorktreeId};
/// A value that can be defined as a user setting. /// A value that can be defined as a user setting.
@@ -167,8 +170,8 @@ pub struct SettingsStore {
raw_user_settings: serde_json::Value, raw_user_settings: serde_json::Value,
raw_server_settings: Option<serde_json::Value>, raw_server_settings: Option<serde_json::Value>,
raw_extension_settings: serde_json::Value, raw_extension_settings: serde_json::Value,
raw_local_settings: raw_local_settings: BTreeMap<(WorktreeId, Arc<Path>), serde_json::Value>,
BTreeMap<(WorktreeId, Arc<Path>), HashMap<LocalSettingsKind, serde_json::Value>>, raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<Path>), (String, Option<Editorconfig>)>,
tab_size_callback: Option<( tab_size_callback: Option<(
TypeId, TypeId,
Box<dyn Fn(&dyn Any) -> Option<usize> + Send + Sync + 'static>, Box<dyn Fn(&dyn Any) -> Option<usize> + Send + Sync + 'static>,
@@ -179,6 +182,26 @@ pub struct SettingsStore {
>, >,
} }
#[derive(Clone)]
pub struct Editorconfig {
pub is_root: bool,
pub sections: SmallVec<[Section; 5]>,
}
impl FromStr for Editorconfig {
type Err = anyhow::Error;
fn from_str(contents: &str) -> Result<Self, Self::Err> {
let parser = ConfigParser::new_buffered(contents.as_bytes())
.context("creating editorconfig parser")?;
let is_root = parser.is_root;
let sections = parser
.collect::<Result<SmallVec<_>, _>>()
.context("parsing editorconfig sections")?;
Ok(Self { is_root, sections })
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum LocalSettingsKind { pub enum LocalSettingsKind {
Settings, Settings,
@@ -226,6 +249,7 @@ impl SettingsStore {
raw_server_settings: None, raw_server_settings: None,
raw_extension_settings: serde_json::json!({}), raw_extension_settings: serde_json::json!({}),
raw_local_settings: Default::default(), raw_local_settings: Default::default(),
raw_editorconfig_settings: BTreeMap::default(),
tab_size_callback: Default::default(), tab_size_callback: Default::default(),
setting_file_updates_tx, setting_file_updates_tx,
_setting_file_updates: cx.spawn(|cx| async move { _setting_file_updates: cx.spawn(|cx| async move {
@@ -567,33 +591,91 @@ impl SettingsStore {
settings_content: Option<&str>, settings_content: Option<&str>,
cx: &mut AppContext, cx: &mut AppContext,
) -> std::result::Result<(), InvalidSettingsError> { ) -> std::result::Result<(), InvalidSettingsError> {
debug_assert!( let mut zed_settings_changed = false;
kind != LocalSettingsKind::Tasks, match (
"Attempted to submit tasks into the settings store" kind,
); settings_content
.map(|content| content.trim())
let raw_local_settings = self .filter(|content| !content.is_empty()),
.raw_local_settings ) {
.entry((root_id, directory_path.clone())) (LocalSettingsKind::Tasks, _) => {
.or_default(); return Err(InvalidSettingsError::Tasks {
let changed = if settings_content.is_some_and(|content| !content.is_empty()) { message: "Attempted to submit tasks into the settings store".to_string(),
let new_contents = })
parse_json_with_comments(settings_content.unwrap()).map_err(|e| { }
InvalidSettingsError::LocalSettings { (LocalSettingsKind::Settings, None) => {
zed_settings_changed = self
.raw_local_settings
.remove(&(root_id, directory_path.clone()))
.is_some()
}
(LocalSettingsKind::Editorconfig, None) => {
self.raw_editorconfig_settings
.remove(&(root_id, directory_path.clone()));
}
(LocalSettingsKind::Settings, Some(settings_contents)) => {
let new_settings = parse_json_with_comments::<serde_json::Value>(settings_contents)
.map_err(|e| InvalidSettingsError::LocalSettings {
path: directory_path.join(local_settings_file_relative_path()), path: directory_path.join(local_settings_file_relative_path()),
message: e.to_string(), message: e.to_string(),
})?;
match self
.raw_local_settings
.entry((root_id, directory_path.clone()))
{
btree_map::Entry::Vacant(v) => {
v.insert(new_settings);
zed_settings_changed = true;
} }
})?; btree_map::Entry::Occupied(mut o) => {
if Some(&new_contents) == raw_local_settings.get(&kind) { if o.get() != &new_settings {
false o.insert(new_settings);
} else { zed_settings_changed = true;
raw_local_settings.insert(kind, new_contents); }
true }
}
}
(LocalSettingsKind::Editorconfig, Some(editorconfig_contents)) => {
match self
.raw_editorconfig_settings
.entry((root_id, directory_path.clone()))
{
btree_map::Entry::Vacant(v) => match editorconfig_contents.parse() {
Ok(new_contents) => {
v.insert((editorconfig_contents.to_owned(), Some(new_contents)));
}
Err(e) => {
v.insert((editorconfig_contents.to_owned(), None));
return Err(InvalidSettingsError::Editorconfig {
message: e.to_string(),
path: directory_path.join(EDITORCONFIG_NAME),
});
}
},
btree_map::Entry::Occupied(mut o) => {
if o.get().0 != editorconfig_contents {
match editorconfig_contents.parse() {
Ok(new_contents) => {
o.insert((
editorconfig_contents.to_owned(),
Some(new_contents),
));
}
Err(e) => {
o.insert((editorconfig_contents.to_owned(), None));
return Err(InvalidSettingsError::Editorconfig {
message: e.to_string(),
path: directory_path.join(EDITORCONFIG_NAME),
});
}
}
}
}
}
} }
} else {
raw_local_settings.remove(&kind).is_some()
}; };
if changed {
if zed_settings_changed {
self.recompute_values(Some((root_id, &directory_path)), cx)?; self.recompute_values(Some((root_id, &directory_path)), cx)?;
} }
Ok(()) Ok(())
@@ -605,13 +687,10 @@ impl SettingsStore {
cx: &mut AppContext, cx: &mut AppContext,
) -> Result<()> { ) -> Result<()> {
let settings: serde_json::Value = serde_json::to_value(content)?; let settings: serde_json::Value = serde_json::to_value(content)?;
if settings.is_object() { anyhow::ensure!(settings.is_object(), "settings must be an object");
self.raw_extension_settings = settings; self.raw_extension_settings = settings;
self.recompute_values(None, cx)?; self.recompute_values(None, cx)?;
Ok(()) Ok(())
} else {
Err(anyhow!("settings must be an object"))
}
} }
/// Add or remove a set of local settings via a JSON string. /// Add or remove a set of local settings via a JSON string.
@@ -625,7 +704,7 @@ impl SettingsStore {
pub fn local_settings( pub fn local_settings(
&self, &self,
root_id: WorktreeId, root_id: WorktreeId,
) -> impl '_ + Iterator<Item = (Arc<Path>, LocalSettingsKind, String)> { ) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
self.raw_local_settings self.raw_local_settings
.range( .range(
(root_id, Path::new("").into()) (root_id, Path::new("").into())
@@ -634,11 +713,23 @@ impl SettingsStore {
Path::new("").into(), Path::new("").into(),
), ),
) )
.flat_map(|((_, path), content)| { .map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
content.iter().filter_map(|(&kind, raw_content)| { }
let parsed_content = serde_json::to_string(raw_content).log_err()?;
Some((path.clone(), kind, parsed_content)) pub fn local_editorconfig_settings(
}) &self,
root_id: WorktreeId,
) -> impl '_ + Iterator<Item = (Arc<Path>, String, Option<Editorconfig>)> {
self.raw_editorconfig_settings
.range(
(root_id, Path::new("").into())
..(
WorktreeId::from_usize(root_id.to_usize() + 1),
Path::new("").into(),
),
)
.map(|((_, path), (content, parsed_content))| {
(path.clone(), content.clone(), parsed_content.clone())
}) })
} }
@@ -753,7 +844,7 @@ impl SettingsStore {
&mut self, &mut self,
changed_local_path: Option<(WorktreeId, &Path)>, changed_local_path: Option<(WorktreeId, &Path)>,
cx: &mut AppContext, cx: &mut AppContext,
) -> Result<(), InvalidSettingsError> { ) -> std::result::Result<(), InvalidSettingsError> {
// Reload the global and local values for every setting. // Reload the global and local values for every setting.
let mut project_settings_stack = Vec::<DeserializedSetting>::new(); let mut project_settings_stack = Vec::<DeserializedSetting>::new();
let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new(); let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
@@ -819,69 +910,90 @@ impl SettingsStore {
paths_stack.clear(); paths_stack.clear();
project_settings_stack.clear(); project_settings_stack.clear();
for ((root_id, directory_path), local_settings) in &self.raw_local_settings { for ((root_id, directory_path), local_settings) in &self.raw_local_settings {
if let Some(local_settings) = local_settings.get(&LocalSettingsKind::Settings) { // Build a stack of all of the local values for that setting.
// Build a stack of all of the local values for that setting. while let Some(prev_entry) = paths_stack.last() {
while let Some(prev_entry) = paths_stack.last() { if let Some((prev_root_id, prev_path)) = prev_entry {
if let Some((prev_root_id, prev_path)) = prev_entry { if root_id != prev_root_id || !directory_path.starts_with(prev_path) {
if root_id != prev_root_id || !directory_path.starts_with(prev_path) { paths_stack.pop();
paths_stack.pop(); project_settings_stack.pop();
project_settings_stack.pop(); continue;
continue;
}
} }
break;
} }
break;
}
match setting_value.deserialize_setting(local_settings) { match setting_value.deserialize_setting(local_settings) {
Ok(local_settings) => { Ok(local_settings) => {
paths_stack.push(Some((*root_id, directory_path.as_ref()))); paths_stack.push(Some((*root_id, directory_path.as_ref())));
project_settings_stack.push(local_settings); project_settings_stack.push(local_settings);
// If a local settings file changed, then avoid recomputing local // If a local settings file changed, then avoid recomputing local
// settings for any path outside of that directory. // settings for any path outside of that directory.
if changed_local_path.map_or( if changed_local_path.map_or(
false, false,
|(changed_root_id, changed_local_path)| { |(changed_root_id, changed_local_path)| {
*root_id != changed_root_id *root_id != changed_root_id
|| !directory_path.starts_with(changed_local_path) || !directory_path.starts_with(changed_local_path)
},
) {
continue;
}
if let Some(value) = setting_value
.load_setting(
SettingsSources {
default: &default_settings,
extensions: extension_settings.as_ref(),
user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(),
server: server_settings.as_ref(),
project: &project_settings_stack.iter().collect::<Vec<_>>(),
}, },
) { cx,
continue; )
} .log_err()
{
if let Some(value) = setting_value setting_value.set_local_value(*root_id, directory_path.clone(), value);
.load_setting(
SettingsSources {
default: &default_settings,
extensions: extension_settings.as_ref(),
user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(),
server: server_settings.as_ref(),
project: &project_settings_stack.iter().collect::<Vec<_>>(),
},
cx,
)
.log_err()
{
setting_value.set_local_value(
*root_id,
directory_path.clone(),
value,
);
}
}
Err(error) => {
return Err(InvalidSettingsError::LocalSettings {
path: directory_path.join(local_settings_file_relative_path()),
message: error.to_string(),
});
} }
} }
Err(error) => {
return Err(InvalidSettingsError::LocalSettings {
path: directory_path.join(local_settings_file_relative_path()),
message: error.to_string(),
});
}
} }
} }
} }
Ok(()) Ok(())
} }
pub fn editorconfg_properties(
&self,
for_worktree: WorktreeId,
for_path: &Path,
) -> Option<EditorconfigProperties> {
let mut properties = EditorconfigProperties::new();
for (directory_with_config, _, parsed_editorconfig) in
self.local_editorconfig_settings(for_worktree)
{
if !for_path.starts_with(&directory_with_config) {
properties.use_fallbacks();
return Some(properties);
}
let parsed_editorconfig = parsed_editorconfig?;
if parsed_editorconfig.is_root {
properties = EditorconfigProperties::new();
}
for section in parsed_editorconfig.sections {
section.apply_to(&mut properties, for_path).log_err()?;
}
}
properties.use_fallbacks();
Some(properties)
}
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@@ -890,6 +1002,8 @@ pub enum InvalidSettingsError {
UserSettings { message: String }, UserSettings { message: String },
ServerSettings { message: String }, ServerSettings { message: String },
DefaultSettings { message: String }, DefaultSettings { message: String },
Editorconfig { path: PathBuf, message: String },
Tasks { message: String },
} }
impl std::fmt::Display for InvalidSettingsError { impl std::fmt::Display for InvalidSettingsError {
@@ -898,8 +1012,10 @@ impl std::fmt::Display for InvalidSettingsError {
InvalidSettingsError::LocalSettings { message, .. } InvalidSettingsError::LocalSettings { message, .. }
| InvalidSettingsError::UserSettings { message } | InvalidSettingsError::UserSettings { message }
| InvalidSettingsError::ServerSettings { message } | InvalidSettingsError::ServerSettings { message }
| InvalidSettingsError::DefaultSettings { message } => { | InvalidSettingsError::DefaultSettings { message }
write!(f, "{}", message) | InvalidSettingsError::Tasks { message }
| InvalidSettingsError::Editorconfig { message, .. } => {
write!(f, "{message}")
} }
} }
} }

View File

@@ -121,7 +121,7 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
let file = buffer.file(); let file = buffer.file();
let language = buffer.language_at(cursor_position); let language = buffer.language_at(cursor_position);
let settings = all_language_settings(file, cx); let settings = all_language_settings(file, cx);
settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref())) settings.inline_completions_enabled(language.as_ref(), file.map(|f| f.path().as_ref()), cx)
} }
fn refresh( fn refresh(

View File

@@ -17,6 +17,8 @@ gpui.workspace = true
settings.workspace = true settings.workspace = true
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true
workspace.workspace = true
menu.workspace = true
[features] [features]
default = [] default = []

View File

@@ -1,3 +1,5 @@
#![allow(unused, dead_code)]
//! # UI Text Field //! # UI Text Field
//! //!
//! This crate provides a text field component that can be used to create text fields like search inputs, form fields, etc. //! This crate provides a text field component that can be used to create text fields like search inputs, form fields, etc.
@@ -5,11 +7,14 @@
//! It can't be located in the `ui` crate because it depends on `editor`. //! It can't be located in the `ui` crate because it depends on `editor`.
//! //!
use std::default;
use editor::*; use editor::*;
use gpui::*; use gpui::*;
use settings::Settings; use settings::Settings;
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::*; use ui::{List, *};
use workspace::{ModalView, Workspace};
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum FieldLabelLayout { pub enum FieldLabelLayout {
@@ -187,3 +192,243 @@ impl Render for TextField {
) )
} }
} }
// -------------------------------------------------------------------------------------------------
actions!(quick_commit, [ToggleStageAll]);
pub const MODAL_WIDTH: f32 = 700.0;
pub const MODAL_HEIGHT: f32 = 300.0;
fn test_files() -> Vec<ChangedFile> {
vec![
ChangedFile {
id: 0,
state: FileVCSState::Modified,
file_name: "file1.txt".into(),
file_path: "/path/to/file1.txt".into(),
},
ChangedFile {
id: 1,
state: FileVCSState::Deleted,
file_name: "file2.txt".into(),
file_path: "/path/to/file2.txt".into(),
},
ChangedFile {
id: 2,
state: FileVCSState::Created,
file_name: "file3.txt".into(),
file_path: "/path/to/file3.txt".into(),
},
]
}
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
enum FileVCSState {
Deleted,
Modified,
Created,
}
struct ChangedFileId(usize);
impl ChangedFileId {
fn new(id: usize) -> Self {
Self(id)
}
}
// placeholder for ui
#[derive(Debug, Clone)]
struct ChangedFile {
id: usize,
state: FileVCSState,
file_name: SharedString,
file_path: SharedString,
}
struct QuickCommitState {
placeholder_text: SharedString,
tracked_files: Vec<ChangedFile>,
staged_files: Vec<usize>,
active_participant_handles: Vec<SharedString>,
editor: View<Editor>,
workspace: WeakView<Workspace>,
}
impl QuickCommitState {
fn init(
editor: View<Editor>,
workspace: WeakView<Workspace>,
cx: &mut ModelContext<Self>,
) -> Self {
let workspace = workspace.clone();
Self {
placeholder_text: "Add a message".into(),
tracked_files: Default::default(),
staged_files: Default::default(),
active_participant_handles: Default::default(),
editor,
workspace,
}
}
fn stage_state(&self) -> Selection {
let staged_files = self.staged_files.clone();
let tracked_files = self.tracked_files.clone();
if staged_files.len() == tracked_files.len() {
Selection::Selected
} else if staged_files.is_empty() {
Selection::Unselected
} else {
Selection::Indeterminate
}
}
fn stage_all(&mut self) -> &mut Self {
let tracked_files = self.tracked_files.clone();
self.staged_files = tracked_files.iter().map(|file| file.id).collect();
self
}
fn toggle_stage_all(&mut self) {
let stage_state = self.stage_state();
let staged_files = self.staged_files.clone();
let tracked_files = self.tracked_files.clone();
match stage_state {
Selection::Selected => {
self.staged_files.clear();
}
Selection::Unselected | Selection::Indeterminate => {
self.stage_all();
}
}
}
fn toggle_file_staged(&mut self, file_id: usize) {
if let Some(pos) = self.staged_files.iter().position() {
self.staged_files.swap_remove(pos);
} else {
self.staged_files.push(file_id);
}
}
}
pub struct QuickCommit {
state: Model<QuickCommitState>,
}
impl QuickCommit {
pub fn init(workspace: WeakView<Workspace>, cx: &mut WindowContext) -> View<Self> {
let editor = cx.new_view(|cx| {
let mut editor = Editor::multi_line(cx);
editor.set_show_gutter(false, cx);
editor
});
cx.new_view(|cx| {
let state = cx
.new_model(move |cx| QuickCommitState::init(editor.clone(), workspace.clone(), cx));
Self { state }
})
}
fn stage_state(&self, cx: &ViewContext<Self>) -> Selection {
self.state.read(cx).stage_state()
}
fn toggle_stage_all(&mut self, _: &ToggleStageAll, cx: &mut ViewContext<Self>) {
self.state.update(cx, |state, _| state.toggle_stage_all());
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent)
}
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.state.read(cx).editor.focus_handle(cx)
}
}
impl QuickCommit {
fn render_file_list(&mut self, cx: &mut ViewContext<Self>) -> List {
List::new().empty_message("No changes")
}
}
impl Render for QuickCommit {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let staged_files = self.state.read(cx).staged_files.clone();
let total_tracked_files = self.state.read(cx).tracked_files.clone();
let staged_state = self.stage_state(cx);
h_flex()
.id("quick_commit_modal")
.key_context("quick_commit")
.track_focus(&self.focus_handle(cx))
.on_action(cx.listener(Self::cancel))
.occlude()
.h(px(MODAL_HEIGHT))
.w(px(MODAL_WIDTH))
.child(
// commit editor
div()
.h_full()
.flex_1()
// .child(self.editor.clone())
.child(
div()
.absolute()
.bottom_2()
.right_2()
.child(Button::new("submit_commit", "Commit")),
),
)
.child(
// file list
div()
.w(relative(0.42))
.h_full()
.border_l_1()
.border_color(cx.theme().colors().border)
// sticky header
.child(
h_flex()
.h_10()
.w_full()
.child(Label::new(format!(
"Staged Files: {}/{}",
staged_files.len(),
total_tracked_files.len()
)))
.child(Checkbox::new("toggle-stage-all", staged_state).on_click(
|_, cx| {
cx.dispatch_action(ToggleStageAll.boxed_clone());
},
)),
)
// file list
.child(self.render_file_list(cx)),
)
}
}
impl EventEmitter<DismissEvent> for QuickCommit {}
impl FocusableView for QuickCommit {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
// TODO: Not sure this is right
self.focus_handle(cx)
}
}
impl ModalView for QuickCommit {
fn fade_out_background(&self) -> bool {
true
}
}

View File

@@ -1739,11 +1739,9 @@ impl Pane {
.worktree_for_entry(entry, cx)? .worktree_for_entry(entry, cx)?
.read(cx); .read(cx);
let entry = worktree.entry_for_id(entry)?; let entry = worktree.entry_for_id(entry)?;
let abs_path = worktree.absolutize(&entry.path).ok()?; match &entry.canonical_path {
if entry.is_symlink { Some(canonical_path) => Some(canonical_path.to_path_buf()),
abs_path.canonicalize().ok() None => worktree.absolutize(&entry.path).ok(),
} else {
Some(abs_path)
} }
} }

View File

@@ -3203,7 +3203,6 @@ pub struct Entry {
pub mtime: Option<SystemTime>, pub mtime: Option<SystemTime>,
pub canonical_path: Option<Box<Path>>, pub canonical_path: Option<Box<Path>>,
pub is_symlink: bool,
/// Whether this entry is ignored by Git. /// Whether this entry is ignored by Git.
/// ///
/// We only scan ignored entries once the directory is expanded and /// We only scan ignored entries once the directory is expanded and
@@ -3280,7 +3279,6 @@ impl Entry {
mtime: Some(metadata.mtime), mtime: Some(metadata.mtime),
size: metadata.len, size: metadata.len,
canonical_path, canonical_path,
is_symlink: metadata.is_symlink,
is_ignored: false, is_ignored: false,
is_external: false, is_external: false,
is_private: false, is_private: false,
@@ -5249,12 +5247,15 @@ impl<'a> From<&'a Entry> for proto::Entry {
path: entry.path.to_string_lossy().into(), path: entry.path.to_string_lossy().into(),
inode: entry.inode, inode: entry.inode,
mtime: entry.mtime.map(|time| time.into()), mtime: entry.mtime.map(|time| time.into()),
is_symlink: entry.is_symlink,
is_ignored: entry.is_ignored, is_ignored: entry.is_ignored,
is_external: entry.is_external, is_external: entry.is_external,
git_status: entry.git_status.map(git_status_to_proto), git_status: entry.git_status.map(git_status_to_proto),
is_fifo: entry.is_fifo, is_fifo: entry.is_fifo,
size: Some(entry.size), size: Some(entry.size),
canonical_path: entry
.canonical_path
.as_ref()
.map(|path| path.to_string_lossy().to_string()),
} }
} }
} }
@@ -5277,12 +5278,13 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
inode: entry.inode, inode: entry.inode,
mtime: entry.mtime.map(|time| time.into()), mtime: entry.mtime.map(|time| time.into()),
size: entry.size.unwrap_or(0), size: entry.size.unwrap_or(0),
canonical_path: None, canonical_path: entry
.canonical_path
.map(|path_string| Box::from(Path::new(&path_string))),
is_ignored: entry.is_ignored, is_ignored: entry.is_ignored,
is_external: entry.is_external, is_external: entry.is_external,
git_status: git_status_from_proto(entry.git_status), git_status: git_status_from_proto(entry.git_status),
is_private: false, is_private: false,
is_symlink: entry.is_symlink,
char_bag, char_bag,
is_fifo: entry.is_fifo, is_fifo: entry.is_fifo,
}) })

View File

@@ -24,7 +24,7 @@ To use a binary in a custom location, add the following to your `settings.json`:
} }
``` ```
If you want to disable Zed looking for a `clangd` binary, you can set `ignore_system-version` to `true`: If you want to disable Zed looking for a `clangd` binary, you can set `ignore_system_version` to `true`:
```json ```json
{ {

View File

@@ -17,7 +17,21 @@ The `OmniSharp` binary can be configured in a Zed settings file with:
"omnisharp": { "omnisharp": {
"binary": { "binary": {
"path": "/path/to/OmniSharp", "path": "/path/to/OmniSharp",
"args": ["optional", "additional", "args", "-lsp"] "arguments": ["optional", "additional", "args", "-lsp"]
}
}
}
}
```
If you want to disable Zed looking for a `omnisharp` binary, you can set `ignore_system_version` to `true`:
```json
{
"lsp": {
"omnisharp": {
"binary": {
"ignore_system_version": true
} }
} }
} }

View File

@@ -1,6 +1,6 @@
# Vue # Vue
Vue support is available through the [Vue extension](https://github.com/zed-industries/zed/tree/main/extensions/vue). Vue support is available through the [Vue extension](https://github.com/zed-extensions/vue).
- Tree Sitter: [tree-sitter-grammars/tree-sitter-vue](https://github.com/tree-sitter-grammars/tree-sitter-vue) - Tree Sitter: [tree-sitter-grammars/tree-sitter-vue](https://github.com/tree-sitter-grammars/tree-sitter-vue)
- Language Server: [vuejs/language-tools/](https://github.com/vuejs/language-tools/) - Language Server: [vuejs/language-tools/](https://github.com/vuejs/language-tools/)