Compare commits
10 Commits
embeddings
...
quick-comm
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
58a7a277df | ||
|
|
5e9f084f12 | ||
|
|
b355a6f449 | ||
|
|
6341ad2f7a | ||
|
|
d3cb08bf35 | ||
|
|
d95a4f8671 | ||
|
|
44dc693d30 | ||
|
|
92c29be74c | ||
|
|
1ae30f5813 | ||
|
|
e8207288e5 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -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"
|
||||||
|
|||||||
2
.github/workflows/publish_extension_cli.yml
vendored
2
.github/workflows/publish_extension_cli.yml
vendored
@@ -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
11
Cargo.lock
generated
@@ -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]]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE worktree_entries ADD COLUMN canonical_path text;
|
||||||
|
ALTER TABLE worktree_entries ALTER COLUMN is_symlink SET DEFAULT false;
|
||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
),]
|
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()]
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
})?)
|
})?)
|
||||||
|
|||||||
@@ -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,
|
||||||
})?)
|
})?)
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}});
|
||||||
|
|||||||
@@ -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)]),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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<()> {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 = []
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/)
|
||||||
|
|||||||
Reference in New Issue
Block a user