Compare commits
11 Commits
git-integr
...
failing-pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2facaa438 | ||
|
|
3b16e28ef5 | ||
|
|
24cbeada2b | ||
|
|
ac520636b6 | ||
|
|
917591da69 | ||
|
|
9368c1ed08 | ||
|
|
8c5b4503d3 | ||
|
|
5b8d126380 | ||
|
|
efbaa3e1a9 | ||
|
|
cf8ce3ed1e | ||
|
|
732d137bcb |
5
.github/actions/run_tests/action.yml
vendored
5
.github/actions/run_tests/action.yml
vendored
@@ -18,6 +18,9 @@ runs:
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: script/clear-target-dir-if-larger-than 100
|
||||
|
||||
# FIXME
|
||||
- name: Run tests
|
||||
shell: bash -euxo pipefail {0}
|
||||
run: cargo nextest run --workspace --no-fail-fast --failure-output immediate-final
|
||||
env:
|
||||
RUST_LOG: project=debug,project_panel=debug
|
||||
run: cargo nextest run -p project_panel --no-fail-fast --failure-output immediate-final --no-capture
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -12078,6 +12078,7 @@ dependencies = [
|
||||
"criterion",
|
||||
"db",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"file_icons",
|
||||
"git",
|
||||
"git_ui",
|
||||
|
||||
@@ -46,6 +46,7 @@ pub trait DapDelegate: Send + Sync + 'static {
|
||||
async fn which(&self, command: &OsStr) -> Option<PathBuf>;
|
||||
async fn read_text_file(&self, path: &RelPath) -> Result<String>;
|
||||
async fn shell_env(&self) -> collections::HashMap<String, String>;
|
||||
fn is_headless(&self) -> bool;
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
||||
@@ -120,6 +120,13 @@ impl JsDebugAdapter {
|
||||
configuration
|
||||
.entry("sourceMapRenames")
|
||||
.or_insert(true.into());
|
||||
|
||||
// Set up remote browser debugging
|
||||
if delegate.is_headless() {
|
||||
configuration
|
||||
.entry("browserLaunchLocation")
|
||||
.or_insert("ui".into());
|
||||
}
|
||||
}
|
||||
|
||||
let adapter_path = if let Some(user_installed_path) = user_installed_path {
|
||||
|
||||
@@ -350,6 +350,7 @@ pub fn init(cx: &mut App) {
|
||||
|
||||
cx.set_global(GlobalBlameRenderer(Arc::new(())));
|
||||
|
||||
dbg!();
|
||||
workspace::register_project_item::<Editor>(cx);
|
||||
workspace::FollowableViewRegistry::register::<Editor>(cx);
|
||||
workspace::register_serializable_item::<Editor>(cx);
|
||||
|
||||
@@ -164,6 +164,7 @@ pub struct BreakpointStore {
|
||||
|
||||
impl BreakpointStore {
|
||||
pub fn init(client: &AnyProtoClient) {
|
||||
log::error!("breakpoint store init");
|
||||
client.add_entity_request_handler(Self::handle_toggle_breakpoint);
|
||||
client.add_entity_message_handler(Self::handle_breakpoints_for_file);
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ use dap::{
|
||||
inline_value::VariableLookupKind,
|
||||
messages::Message,
|
||||
};
|
||||
use fs::Fs;
|
||||
use fs::{Fs, RemoveOptions};
|
||||
use futures::{
|
||||
StreamExt,
|
||||
StreamExt, TryStreamExt as _,
|
||||
channel::mpsc::{self, UnboundedSender},
|
||||
future::{Shared, join_all},
|
||||
};
|
||||
@@ -78,12 +78,15 @@ pub struct LocalDapStore {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
environment: Entity<ProjectEnvironment>,
|
||||
toolchain_store: Arc<dyn LanguageToolchainStore>,
|
||||
is_headless: bool,
|
||||
}
|
||||
|
||||
pub struct RemoteDapStore {
|
||||
remote_client: Entity<RemoteClient>,
|
||||
upstream_client: AnyProtoClient,
|
||||
upstream_project_id: u64,
|
||||
node_runtime: NodeRuntime,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
}
|
||||
|
||||
pub struct DapStore {
|
||||
@@ -134,17 +137,19 @@ impl DapStore {
|
||||
toolchain_store: Arc<dyn LanguageToolchainStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
is_headless: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mode = DapStoreMode::Local(LocalDapStore {
|
||||
fs,
|
||||
fs: fs.clone(),
|
||||
environment,
|
||||
http_client,
|
||||
node_runtime,
|
||||
toolchain_store,
|
||||
is_headless,
|
||||
});
|
||||
|
||||
Self::new(mode, breakpoint_store, worktree_store, cx)
|
||||
Self::new(mode, breakpoint_store, worktree_store, fs, cx)
|
||||
}
|
||||
|
||||
pub fn new_remote(
|
||||
@@ -152,15 +157,20 @@ impl DapStore {
|
||||
remote_client: Entity<RemoteClient>,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
node_runtime: NodeRuntime,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mode = DapStoreMode::Remote(RemoteDapStore {
|
||||
upstream_client: remote_client.read(cx).proto_client(),
|
||||
remote_client,
|
||||
upstream_project_id: project_id,
|
||||
node_runtime,
|
||||
http_client,
|
||||
});
|
||||
|
||||
Self::new(mode, breakpoint_store, worktree_store, cx)
|
||||
Self::new(mode, breakpoint_store, worktree_store, fs, cx)
|
||||
}
|
||||
|
||||
pub fn new_collab(
|
||||
@@ -168,17 +178,55 @@ impl DapStore {
|
||||
_upstream_client: AnyProtoClient,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new(DapStoreMode::Collab, breakpoint_store, worktree_store, cx)
|
||||
Self::new(
|
||||
DapStoreMode::Collab,
|
||||
breakpoint_store,
|
||||
worktree_store,
|
||||
fs,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn new(
|
||||
mode: DapStoreMode,
|
||||
breakpoint_store: Entity<BreakpointStore>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
_cx: &mut Context<Self>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
cx.background_spawn(async move {
|
||||
let dir = paths::debug_adapters_dir().join("js-debug-companion");
|
||||
|
||||
let mut children = fs.read_dir(&dir).await?.try_collect::<Vec<_>>().await?;
|
||||
children.sort_by_key(|child| semver::Version::parse(child.file_name()?.to_str()?).ok());
|
||||
|
||||
if let Some(child) = children.last()
|
||||
&& let Some(name) = child.file_name()
|
||||
&& let Some(name) = name.to_str()
|
||||
&& semver::Version::parse(name).is_ok()
|
||||
{
|
||||
children.pop();
|
||||
}
|
||||
|
||||
for child in children {
|
||||
fs.remove_dir(
|
||||
&child,
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: true,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
mode,
|
||||
next_session_id: 0,
|
||||
@@ -401,6 +449,15 @@ impl DapStore {
|
||||
});
|
||||
}
|
||||
|
||||
let (remote_client, node_runtime, http_client) = match &self.mode {
|
||||
DapStoreMode::Local(_) => (None, None, None),
|
||||
DapStoreMode::Remote(remote_dap_store) => (
|
||||
Some(remote_dap_store.remote_client.clone()),
|
||||
Some(remote_dap_store.node_runtime.clone()),
|
||||
Some(remote_dap_store.http_client.clone()),
|
||||
),
|
||||
DapStoreMode::Collab => (None, None, None),
|
||||
};
|
||||
let session = Session::new(
|
||||
self.breakpoint_store.clone(),
|
||||
session_id,
|
||||
@@ -409,6 +466,9 @@ impl DapStore {
|
||||
adapter,
|
||||
task_context,
|
||||
quirks,
|
||||
remote_client,
|
||||
node_runtime,
|
||||
http_client,
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -538,6 +598,7 @@ impl DapStore {
|
||||
local_store.environment.update(cx, |env, cx| {
|
||||
env.get_worktree_environment(worktree.clone(), cx)
|
||||
}),
|
||||
local_store.is_headless,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -870,6 +931,7 @@ pub struct DapAdapterDelegate {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
toolchain_store: Arc<dyn LanguageToolchainStore>,
|
||||
load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
|
||||
is_headless: bool,
|
||||
}
|
||||
|
||||
impl DapAdapterDelegate {
|
||||
@@ -881,6 +943,7 @@ impl DapAdapterDelegate {
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
toolchain_store: Arc<dyn LanguageToolchainStore>,
|
||||
load_shell_env_task: Shared<Task<Option<HashMap<String, String>>>>,
|
||||
is_headless: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
fs,
|
||||
@@ -890,6 +953,7 @@ impl DapAdapterDelegate {
|
||||
node_runtime,
|
||||
toolchain_store,
|
||||
load_shell_env_task,
|
||||
is_headless,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -953,4 +1017,8 @@ impl dap::adapters::DapDelegate for DapAdapterDelegate {
|
||||
|
||||
self.fs.load(&abs_path).await
|
||||
}
|
||||
|
||||
fn is_headless(&self) -> bool {
|
||||
self.is_headless
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,21 +31,28 @@ use dap::{
|
||||
RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments,
|
||||
StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments,
|
||||
};
|
||||
use futures::SinkExt;
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use futures::channel::{mpsc, oneshot};
|
||||
use futures::io::BufReader;
|
||||
use futures::{AsyncBufReadExt as _, SinkExt, StreamExt, TryStreamExt};
|
||||
use futures::{FutureExt, future::Shared};
|
||||
use gpui::{
|
||||
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString,
|
||||
Task, WeakEntity,
|
||||
};
|
||||
use http_client::HttpClient;
|
||||
|
||||
use node_runtime::NodeRuntime;
|
||||
use remote::RemoteClient;
|
||||
use rpc::ErrorExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use smol::stream::StreamExt;
|
||||
use smol::net::TcpListener;
|
||||
use std::any::TypeId;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use std::u64;
|
||||
use std::{
|
||||
any::Any,
|
||||
@@ -56,6 +63,7 @@ use std::{
|
||||
};
|
||||
use task::TaskContext;
|
||||
use text::{PointUtf16, ToPointUtf16};
|
||||
use util::command::new_smol_command;
|
||||
use util::{ResultExt, debug_panic, maybe};
|
||||
use worktree::Worktree;
|
||||
|
||||
@@ -696,6 +704,10 @@ pub struct Session {
|
||||
task_context: TaskContext,
|
||||
memory: memory::Memory,
|
||||
quirks: SessionQuirks,
|
||||
remote_client: Option<Entity<RemoteClient>>,
|
||||
node_runtime: Option<NodeRuntime>,
|
||||
http_client: Option<Arc<dyn HttpClient>>,
|
||||
companion_port: Option<u16>,
|
||||
}
|
||||
|
||||
trait CacheableCommand: Any + Send + Sync {
|
||||
@@ -812,6 +824,9 @@ impl Session {
|
||||
adapter: DebugAdapterName,
|
||||
task_context: TaskContext,
|
||||
quirks: SessionQuirks,
|
||||
remote_client: Option<Entity<RemoteClient>>,
|
||||
node_runtime: Option<NodeRuntime>,
|
||||
http_client: Option<Arc<dyn HttpClient>>,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
cx.new::<Self>(|cx| {
|
||||
@@ -867,6 +882,10 @@ impl Session {
|
||||
task_context,
|
||||
memory: memory::Memory::new(),
|
||||
quirks,
|
||||
remote_client,
|
||||
node_runtime,
|
||||
http_client,
|
||||
companion_port: None,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1557,7 +1576,21 @@ impl Session {
|
||||
Events::ProgressStart(_) => {}
|
||||
Events::ProgressUpdate(_) => {}
|
||||
Events::Invalidated(_) => {}
|
||||
Events::Other(_) => {}
|
||||
Events::Other(event) => {
|
||||
if event.event == "launchBrowserInCompanion" {
|
||||
let Some(request) = serde_json::from_value(event.body).ok() else {
|
||||
log::error!("failed to deserialize launchBrowserInCompanion event");
|
||||
return;
|
||||
};
|
||||
self.launch_browser_for_remote_server(request, cx);
|
||||
} else if event.event == "killCompanionBrowser" {
|
||||
let Some(request) = serde_json::from_value(event.body).ok() else {
|
||||
log::error!("failed to deserialize killCompanionBrowser event");
|
||||
return;
|
||||
};
|
||||
self.kill_browser(request, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2716,4 +2749,304 @@ impl Session {
|
||||
pub fn quirks(&self) -> SessionQuirks {
|
||||
self.quirks
|
||||
}
|
||||
|
||||
fn launch_browser_for_remote_server(
|
||||
&mut self,
|
||||
mut request: LaunchBrowserInCompanionParams,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(remote_client) = self.remote_client.clone() else {
|
||||
log::error!("can't launch browser in companion for non-remote project");
|
||||
return;
|
||||
};
|
||||
let Some(http_client) = self.http_client.clone() else {
|
||||
return;
|
||||
};
|
||||
let Some(node_runtime) = self.node_runtime.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut console_output = self.console_output(cx);
|
||||
let task = cx.spawn(async move |this, cx| {
|
||||
let (dap_port, _child) =
|
||||
if remote_client.read_with(cx, |client, _| client.shares_network_interface())? {
|
||||
(request.server_port, None)
|
||||
} else {
|
||||
let port = {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.context("getting port for DAP")?;
|
||||
listener.local_addr()?.port()
|
||||
};
|
||||
let child = remote_client.update(cx, |client, _| {
|
||||
let command = client.build_forward_port_command(
|
||||
port,
|
||||
"localhost".into(),
|
||||
request.server_port,
|
||||
)?;
|
||||
let child = new_smol_command(command.program)
|
||||
.args(command.args)
|
||||
.envs(command.env)
|
||||
.spawn()
|
||||
.context("spawning port forwarding process")?;
|
||||
anyhow::Ok(child)
|
||||
})??;
|
||||
(port, Some(child))
|
||||
};
|
||||
|
||||
let mut companion_process = None;
|
||||
let companion_port =
|
||||
if let Some(companion_port) = this.read_with(cx, |this, _| this.companion_port)? {
|
||||
companion_port
|
||||
} else {
|
||||
let task = cx.spawn(async move |cx| spawn_companion(node_runtime, cx).await);
|
||||
match task.await {
|
||||
Ok((port, child)) => {
|
||||
companion_process = Some(child);
|
||||
port
|
||||
}
|
||||
Err(e) => {
|
||||
console_output
|
||||
.send(format!("Failed to launch browser companion process: {e}"))
|
||||
.await
|
||||
.ok();
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.update(cx, |this, cx| {
|
||||
this.companion_port = Some(companion_port);
|
||||
let Some(mut child) = companion_process else {
|
||||
return;
|
||||
};
|
||||
if let Some(stderr) = child.stderr.take() {
|
||||
let mut console_output = console_output.clone();
|
||||
this.background_tasks.push(cx.spawn(async move |_, _| {
|
||||
let mut stderr = BufReader::new(stderr);
|
||||
let mut line = String::new();
|
||||
while let Ok(n) = stderr.read_line(&mut line).await
|
||||
&& n > 0
|
||||
{
|
||||
console_output
|
||||
.send(format!("companion stderr: {line}"))
|
||||
.await
|
||||
.ok();
|
||||
line.clear();
|
||||
}
|
||||
}));
|
||||
}
|
||||
this.background_tasks.push(cx.spawn({
|
||||
let mut console_output = console_output.clone();
|
||||
async move |_, _| match child.status().await {
|
||||
Ok(status) => {
|
||||
if status.success() {
|
||||
console_output
|
||||
.send("Companion process exited normally".into())
|
||||
.await
|
||||
.ok();
|
||||
} else {
|
||||
console_output
|
||||
.send(format!(
|
||||
"Companion process exited abnormally with {status:?}"
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
console_output
|
||||
.send(format!("Failed to join companion process: {e}"))
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}))
|
||||
})?;
|
||||
|
||||
request
|
||||
.other
|
||||
.insert("proxyUri".into(), format!("127.0.0.1:{dap_port}").into());
|
||||
// TODO pass wslInfo as needed
|
||||
|
||||
let response = http_client
|
||||
.post_json(
|
||||
&format!("http://127.0.0.1:{companion_port}/launch-and-attach"),
|
||||
serde_json::to_string(&request)
|
||||
.context("serializing request")?
|
||||
.into(),
|
||||
)
|
||||
.await;
|
||||
match response {
|
||||
Ok(response) => {
|
||||
if !response.status().is_success() {
|
||||
console_output
|
||||
.send("Launch request to companion failed".into())
|
||||
.await
|
||||
.ok();
|
||||
return Err(anyhow!("launch request failed"));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
console_output
|
||||
.send("Failed to read response from companion".into())
|
||||
.await
|
||||
.ok();
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
});
|
||||
self.background_tasks.push(cx.spawn(async move |_, _| {
|
||||
task.await.log_err();
|
||||
}));
|
||||
}
|
||||
|
||||
fn kill_browser(&self, request: KillCompanionBrowserParams, cx: &mut App) {
|
||||
let Some(companion_port) = self.companion_port else {
|
||||
log::error!("received killCompanionBrowser but js-debug-companion is not running");
|
||||
return;
|
||||
};
|
||||
let Some(http_client) = self.http_client.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
http_client
|
||||
.post_json(
|
||||
&format!("http://127.0.0.1:{companion_port}/kill"),
|
||||
serde_json::to_string(&request)
|
||||
.context("serializing request")?
|
||||
.into(),
|
||||
)
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct LaunchBrowserInCompanionParams {
|
||||
server_port: u16,
|
||||
#[serde(flatten)]
|
||||
other: HashMap<String, serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KillCompanionBrowserParams {
|
||||
launch_id: u64,
|
||||
}
|
||||
|
||||
async fn spawn_companion(
|
||||
node_runtime: NodeRuntime,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<(u16, smol::process::Child)> {
|
||||
let binary_path = node_runtime
|
||||
.binary_path()
|
||||
.await
|
||||
.context("getting node path")?;
|
||||
let path = cx
|
||||
.spawn(async move |cx| get_or_install_companion(node_runtime, cx).await)
|
||||
.await?;
|
||||
log::info!("will launch js-debug-companion version {path:?}");
|
||||
|
||||
let port = {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.context("getting port for companion")?;
|
||||
listener.local_addr()?.port()
|
||||
};
|
||||
|
||||
let dir = paths::data_dir()
|
||||
.join("js_debug_companion_state")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let child = new_smol_command(binary_path)
|
||||
.arg(path)
|
||||
.args([
|
||||
format!("--listen=127.0.0.1:{port}"),
|
||||
format!("--state={dir}"),
|
||||
])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.context("spawning companion child process")?;
|
||||
|
||||
Ok((port, child))
|
||||
}
|
||||
|
||||
async fn get_or_install_companion(node: NodeRuntime, cx: &mut AsyncApp) -> Result<PathBuf> {
|
||||
const PACKAGE_NAME: &str = "@zed-industries/js-debug-companion-cli";
|
||||
|
||||
async fn install_latest_version(dir: PathBuf, node: NodeRuntime) -> Result<PathBuf> {
|
||||
let temp_dir = tempfile::tempdir().context("creating temporary directory")?;
|
||||
node.npm_install_packages(temp_dir.path(), &[(PACKAGE_NAME, "latest")])
|
||||
.await
|
||||
.context("installing latest companion package")?;
|
||||
let version = node
|
||||
.npm_package_installed_version(temp_dir.path(), PACKAGE_NAME)
|
||||
.await
|
||||
.context("getting installed companion version")?
|
||||
.context("companion was not installed")?;
|
||||
smol::fs::rename(temp_dir.path(), dir.join(&version))
|
||||
.await
|
||||
.context("moving companion package into place")?;
|
||||
Ok(dir.join(version))
|
||||
}
|
||||
|
||||
let dir = paths::debug_adapters_dir().join("js-debug-companion");
|
||||
let (latest_installed_version, latest_version) = cx
|
||||
.background_spawn({
|
||||
let dir = dir.clone();
|
||||
let node = node.clone();
|
||||
async move {
|
||||
smol::fs::create_dir_all(&dir)
|
||||
.await
|
||||
.context("creating companion installation directory")?;
|
||||
|
||||
let mut children = smol::fs::read_dir(&dir)
|
||||
.await
|
||||
.context("reading companion installation directory")?
|
||||
.try_collect::<Vec<_>>()
|
||||
.await
|
||||
.context("reading companion installation directory entries")?;
|
||||
children
|
||||
.sort_by_key(|child| semver::Version::parse(child.file_name().to_str()?).ok());
|
||||
|
||||
let latest_installed_version = children.last().and_then(|child| {
|
||||
let version = child.file_name().into_string().ok()?;
|
||||
Some((child.path(), version))
|
||||
});
|
||||
let latest_version = node
|
||||
.npm_package_latest_version(PACKAGE_NAME)
|
||||
.await
|
||||
.log_err();
|
||||
anyhow::Ok((latest_installed_version, latest_version))
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
let path = if let Some((installed_path, installed_version)) = latest_installed_version {
|
||||
if let Some(latest_version) = latest_version
|
||||
&& latest_version != installed_version
|
||||
{
|
||||
cx.background_spawn(install_latest_version(dir.clone(), node.clone()))
|
||||
.detach();
|
||||
}
|
||||
Ok(installed_path)
|
||||
} else {
|
||||
cx.background_spawn(install_latest_version(dir.clone(), node.clone()))
|
||||
.await
|
||||
};
|
||||
|
||||
Ok(path?
|
||||
.join("node_modules")
|
||||
.join(PACKAGE_NAME)
|
||||
.join("out")
|
||||
.join("cli.js"))
|
||||
}
|
||||
|
||||
@@ -1084,6 +1084,7 @@ impl Project {
|
||||
toolchain_store.read(cx).as_language_toolchain_store(),
|
||||
worktree_store.clone(),
|
||||
breakpoint_store.clone(),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1306,6 +1307,9 @@ impl Project {
|
||||
remote.clone(),
|
||||
breakpoint_store.clone(),
|
||||
worktree_store.clone(),
|
||||
node.clone(),
|
||||
client.http_client(),
|
||||
fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -1503,6 +1507,7 @@ impl Project {
|
||||
client.clone().into(),
|
||||
breakpoint_store.clone(),
|
||||
worktree_store.clone(),
|
||||
fs.clone(),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
@@ -51,6 +51,7 @@ workspace-hack.workspace = true
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
criterion.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
language = { workspace = true, features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -102,7 +102,7 @@ impl State {
|
||||
max_width_item_index: None,
|
||||
edit_state: old.edit_state.clone(),
|
||||
unfolded_dir_ids: old.unfolded_dir_ids.clone(),
|
||||
selection: old.selection,
|
||||
selection: dbg!(old.selection),
|
||||
expanded_dir_ids: old.expanded_dir_ids.clone(),
|
||||
}
|
||||
}
|
||||
@@ -398,6 +398,7 @@ pub fn init(cx: &mut App) {
|
||||
if let Some(first_marked) = panel.marked_entries.first() {
|
||||
let first_marked = *first_marked;
|
||||
panel.marked_entries.clear();
|
||||
dbg!();
|
||||
panel.state.selection = Some(first_marked);
|
||||
}
|
||||
panel.rename(action, window, cx);
|
||||
@@ -729,8 +730,10 @@ impl ProjectPanel {
|
||||
focus_opened_item,
|
||||
allow_preview,
|
||||
} => {
|
||||
dbg!();
|
||||
if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx)
|
||||
&& let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
|
||||
dbg!();
|
||||
let file_path = entry.path.clone();
|
||||
let worktree_id = worktree.read(cx).id();
|
||||
let entry_id = entry.id;
|
||||
@@ -767,12 +770,13 @@ impl ProjectPanel {
|
||||
});
|
||||
|
||||
if let Some(project_panel) = project_panel.upgrade() {
|
||||
dbg!();
|
||||
// Always select and mark the entry, regardless of whether it is opened or not.
|
||||
project_panel.update(cx, |project_panel, _| {
|
||||
let entry = SelectedEntry { worktree_id, entry_id };
|
||||
project_panel.marked_entries.clear();
|
||||
project_panel.marked_entries.push(entry);
|
||||
project_panel.state.selection = Some(entry);
|
||||
project_panel.state.selection = Some(dbg!(entry));
|
||||
});
|
||||
if !focus_opened_item {
|
||||
let focus_handle = project_panel.read(cx).focus_handle.clone();
|
||||
@@ -957,6 +961,7 @@ impl ProjectPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
dbg!();
|
||||
self.state.selection = Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
@@ -1398,6 +1403,7 @@ impl ProjectPanel {
|
||||
worktree_id: *worktree_id,
|
||||
entry_id: entries[entry_ix].id,
|
||||
};
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
if window.modifiers().shift {
|
||||
self.marked_entries.push(selection);
|
||||
@@ -1575,6 +1581,7 @@ impl ProjectPanel {
|
||||
let edit_task;
|
||||
let edited_entry_id;
|
||||
if is_new_entry {
|
||||
dbg!();
|
||||
self.state.selection = Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id: NEW_ENTRY_ID,
|
||||
@@ -1629,6 +1636,7 @@ impl ProjectPanel {
|
||||
project_panel.update_in( cx, |project_panel, window, cx| {
|
||||
if let Some(selection) = &mut project_panel.state.selection
|
||||
&& selection.entry_id == edited_entry_id {
|
||||
dbg!();
|
||||
selection.worktree_id = worktree_id;
|
||||
selection.entry_id = new_entry.id;
|
||||
project_panel.marked_entries.clear();
|
||||
@@ -1689,6 +1697,7 @@ impl ProjectPanel {
|
||||
if let Some(previously_focused) =
|
||||
previous_edit_state.and_then(|edit_state| edit_state.previously_focused)
|
||||
{
|
||||
dbg!();
|
||||
self.state.selection = Some(previously_focused);
|
||||
self.autoscroll(cx);
|
||||
}
|
||||
@@ -1749,6 +1758,7 @@ impl ProjectPanel {
|
||||
.read(cx)
|
||||
.id();
|
||||
|
||||
dbg!();
|
||||
self.state.selection = Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
@@ -2253,6 +2263,7 @@ impl ProjectPanel {
|
||||
worktree_id: *worktree_id,
|
||||
entry_id: entry.id,
|
||||
};
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
if window.modifiers().shift {
|
||||
self.marked_entries.push(selection);
|
||||
@@ -2292,6 +2303,7 @@ impl ProjectPanel {
|
||||
);
|
||||
|
||||
if let Some(selection) = selection {
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
self.expand_entry(selection.worktree_id, selection.entry_id, cx);
|
||||
self.update_visible_entries(
|
||||
@@ -2331,6 +2343,7 @@ impl ProjectPanel {
|
||||
);
|
||||
|
||||
if let Some(selection) = selection {
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
self.expand_entry(selection.worktree_id, selection.entry_id, cx);
|
||||
self.update_visible_entries(
|
||||
@@ -2369,6 +2382,7 @@ impl ProjectPanel {
|
||||
);
|
||||
|
||||
if let Some(selection) = selection {
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
self.expand_entry(selection.worktree_id, selection.entry_id, cx);
|
||||
self.update_visible_entries(
|
||||
@@ -2404,6 +2418,7 @@ impl ProjectPanel {
|
||||
);
|
||||
|
||||
if let Some(selection) = selection {
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
self.autoscroll(cx);
|
||||
cx.notify();
|
||||
@@ -2432,6 +2447,7 @@ impl ProjectPanel {
|
||||
);
|
||||
|
||||
if let Some(selection) = selection {
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
self.autoscroll(cx);
|
||||
cx.notify();
|
||||
@@ -2461,6 +2477,7 @@ impl ProjectPanel {
|
||||
);
|
||||
|
||||
if let Some(selection) = selection {
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
self.expand_entry(selection.worktree_id, selection.entry_id, cx);
|
||||
self.update_visible_entries(
|
||||
@@ -2479,6 +2496,7 @@ impl ProjectPanel {
|
||||
if let Some(parent) = entry.path.parent() {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Some(parent_entry) = worktree.entry_for_path(parent) {
|
||||
dbg!();
|
||||
self.state.selection = Some(SelectedEntry {
|
||||
worktree_id: worktree.id(),
|
||||
entry_id: parent_entry.id,
|
||||
@@ -2504,6 +2522,7 @@ impl ProjectPanel {
|
||||
worktree_id: *worktree_id,
|
||||
entry_id: entry.id,
|
||||
};
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
if window.modifiers().shift {
|
||||
self.marked_entries.push(selection);
|
||||
@@ -2528,6 +2547,7 @@ impl ProjectPanel {
|
||||
worktree_id: *worktree_id,
|
||||
entry_id: entry.id,
|
||||
};
|
||||
dbg!();
|
||||
self.state.selection = Some(selection);
|
||||
self.autoscroll(cx);
|
||||
cx.notify();
|
||||
@@ -2679,16 +2699,19 @@ impl ProjectPanel {
|
||||
}
|
||||
// update selection
|
||||
if let Some(entry) = last_succeed {
|
||||
dbg!();
|
||||
project_panel
|
||||
.update_in(cx, |project_panel, window, cx| {
|
||||
dbg!();
|
||||
project_panel.state.selection = Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id: entry.id,
|
||||
entry_id: dbg!(entry.id),
|
||||
});
|
||||
|
||||
if item_count == 1 {
|
||||
// open entry if not dir, and only focus if rename is not pending
|
||||
if !entry.is_dir() {
|
||||
if !dbg!(entry.is_dir()) {
|
||||
dbg!();
|
||||
project_panel.open_entry(
|
||||
entry.id,
|
||||
disambiguation_range.is_none(),
|
||||
@@ -2699,6 +2722,7 @@ impl ProjectPanel {
|
||||
|
||||
// if only one entry was pasted and it was disambiguated, open the rename editor
|
||||
if disambiguation_range.is_some() {
|
||||
dbg!();
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.rename_impl(disambiguation_range, window, cx);
|
||||
});
|
||||
@@ -3192,6 +3216,7 @@ impl ProjectPanel {
|
||||
|
||||
let old_ancestors = self.state.ancestors.clone();
|
||||
let mut new_state = State::derive(&self.state);
|
||||
dbg!(&new_state.selection);
|
||||
new_state.last_worktree_root_id = project
|
||||
.visible_worktrees(cx)
|
||||
.next_back()
|
||||
@@ -3419,15 +3444,17 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
if let Some((worktree_id, entry_id)) = new_selected_entry {
|
||||
new_state.selection = Some(SelectedEntry {
|
||||
dbg!();
|
||||
new_state.selection = Some(dbg!(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
});
|
||||
}));
|
||||
}
|
||||
new_state
|
||||
})
|
||||
.await;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
dbg!();
|
||||
this.state = new_state;
|
||||
let elapsed = now.elapsed();
|
||||
if this.last_reported_update.elapsed() > Duration::from_secs(3600) {
|
||||
@@ -3639,6 +3666,7 @@ impl ProjectPanel {
|
||||
if let Some(entry_id) = last_succeed {
|
||||
project_panel
|
||||
.update_in(cx, |project_panel, window, cx| {
|
||||
dbg!();
|
||||
project_panel.state.selection = Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
@@ -4593,6 +4621,7 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
|
||||
dbg!();
|
||||
project_panel.state.selection = Some(clicked_entry);
|
||||
if !project_panel.marked_entries.contains(&clicked_entry) {
|
||||
project_panel.marked_entries.push(clicked_entry);
|
||||
@@ -4602,6 +4631,7 @@ impl ProjectPanel {
|
||||
if event.click_count() > 1 {
|
||||
project_panel.split_entry(entry_id, false, None, cx);
|
||||
} else {
|
||||
dbg!();
|
||||
project_panel.state.selection = Some(selection);
|
||||
if let Some(position) = project_panel.marked_entries.iter().position(|e| *e == selection) {
|
||||
project_panel.marked_entries.remove(position);
|
||||
@@ -4988,6 +5018,9 @@ impl ProjectPanel {
|
||||
};
|
||||
let is_marked = self.marked_entries.contains(&selection);
|
||||
let is_selected = self.state.selection == Some(selection);
|
||||
if is_selected {
|
||||
dbg!(&entry.path, &self.state.selection);
|
||||
}
|
||||
|
||||
let diagnostic_severity = self
|
||||
.diagnostics
|
||||
@@ -5452,6 +5485,7 @@ impl Render for ProjectPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
dbg!();
|
||||
this.state.selection = Some(SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id,
|
||||
|
||||
@@ -1445,6 +1445,10 @@ async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContex
|
||||
async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
cx.update(|cx| {
|
||||
register_project_item::<TestProjectItemView>(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
"/root1",
|
||||
@@ -1489,9 +1493,16 @@ async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppConte
|
||||
select_path(&panel, "root2/one.txt", cx);
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.select_next(&Default::default(), window, cx);
|
||||
panel.paste(&Default::default(), window, cx);
|
||||
dbg!();
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
dbg!(visible_entries_as_strings(&panel, 0..50, cx));
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
panel.paste(&Default::default(), window, cx);
|
||||
dbg!();
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
dbg!();
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..50, cx),
|
||||
&[
|
||||
@@ -3103,6 +3114,7 @@ async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) {
|
||||
let project = panel.project.read(cx);
|
||||
let worktree = project.visible_worktrees(cx).next().unwrap();
|
||||
let root_entry = worktree.read(cx).root_entry().unwrap();
|
||||
dbg!();
|
||||
panel.state.selection = Some(SelectedEntry {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
entry_id: root_entry.id,
|
||||
@@ -6683,7 +6695,7 @@ fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestCont
|
||||
panel.update_in(cx, |panel, window, cx| {
|
||||
for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
|
||||
let worktree = worktree.read(cx);
|
||||
if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
|
||||
if let Ok(relative_path) = path.strip_prefix(dbg!(worktree.root_name())) {
|
||||
let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
|
||||
panel.update_visible_entries(
|
||||
Some((worktree.id(), entry_id)),
|
||||
@@ -6692,6 +6704,7 @@ fn select_path(panel: &Entity<ProjectPanel>, path: &str, cx: &mut VisualTestCont
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
dbg!(path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -6714,6 +6727,7 @@ fn select_path_with_mark(panel: &Entity<ProjectPanel>, path: &str, cx: &mut Visu
|
||||
if !panel.marked_entries.contains(&entry) {
|
||||
panel.marked_entries.push(entry);
|
||||
}
|
||||
dbg!();
|
||||
panel.state.selection = Some(entry);
|
||||
return;
|
||||
}
|
||||
@@ -6799,6 +6813,7 @@ fn visible_entries_as_strings(
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
env_logger::try_init().ok();
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
init_settings(cx);
|
||||
|
||||
@@ -836,6 +836,18 @@ impl RemoteClient {
|
||||
connection.build_command(program, args, env, working_dir, port_forward)
|
||||
}
|
||||
|
||||
pub fn build_forward_port_command(
|
||||
&self,
|
||||
local_port: u16,
|
||||
host: String,
|
||||
remote_port: u16,
|
||||
) -> Result<CommandTemplate> {
|
||||
let Some(connection) = self.remote_connection() else {
|
||||
return Err(anyhow!("no ssh connection"));
|
||||
};
|
||||
connection.build_forward_port_command(local_port, host, remote_port)
|
||||
}
|
||||
|
||||
pub fn upload_directory(
|
||||
&self,
|
||||
src_path: PathBuf,
|
||||
@@ -1104,6 +1116,12 @@ pub(crate) trait RemoteConnection: Send + Sync {
|
||||
working_dir: Option<String>,
|
||||
port_forward: Option<(u16, String, u16)>,
|
||||
) -> Result<CommandTemplate>;
|
||||
fn build_forward_port_command(
|
||||
&self,
|
||||
local_port: u16,
|
||||
remote: String,
|
||||
remote_port: u16,
|
||||
) -> Result<CommandTemplate>;
|
||||
fn connection_options(&self) -> RemoteConnectionOptions;
|
||||
fn path_style(&self) -> PathStyle;
|
||||
fn shell(&self) -> String;
|
||||
@@ -1533,6 +1551,23 @@ mod fake {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_forward_port_command(
|
||||
&self,
|
||||
local_port: u16,
|
||||
host: String,
|
||||
remote_port: u16,
|
||||
) -> anyhow::Result<CommandTemplate> {
|
||||
Ok(CommandTemplate {
|
||||
program: "ssh".into(),
|
||||
args: vec![
|
||||
"-N".into(),
|
||||
"-L".into(),
|
||||
format!("{local_port}:{host}:{remote_port}"),
|
||||
],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn upload_directory(
|
||||
&self,
|
||||
_src_path: PathBuf,
|
||||
|
||||
@@ -146,6 +146,23 @@ impl RemoteConnection for SshRemoteConnection {
|
||||
)
|
||||
}
|
||||
|
||||
fn build_forward_port_command(
|
||||
&self,
|
||||
local_port: u16,
|
||||
host: String,
|
||||
remote_port: u16,
|
||||
) -> Result<CommandTemplate> {
|
||||
Ok(CommandTemplate {
|
||||
program: "ssh".into(),
|
||||
args: vec![
|
||||
"-N".into(),
|
||||
"-L".into(),
|
||||
format!("{local_port}:{host}:{remote_port}"),
|
||||
],
|
||||
env: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn upload_directory(
|
||||
&self,
|
||||
src_path: PathBuf,
|
||||
|
||||
@@ -433,6 +433,15 @@ impl RemoteConnection for WslRemoteConnection {
|
||||
})
|
||||
}
|
||||
|
||||
fn build_forward_port_command(
|
||||
&self,
|
||||
_: u16,
|
||||
_: String,
|
||||
_: u16,
|
||||
) -> anyhow::Result<CommandTemplate> {
|
||||
Err(anyhow!("WSL shares a network interface with the host"))
|
||||
}
|
||||
|
||||
fn connection_options(&self) -> RemoteConnectionOptions {
|
||||
RemoteConnectionOptions::Wsl(self.connection_options.clone())
|
||||
}
|
||||
|
||||
@@ -123,6 +123,7 @@ impl HeadlessProject {
|
||||
toolchain_store.read(cx).as_language_toolchain_store(),
|
||||
worktree_store.clone(),
|
||||
breakpoint_store.clone(),
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx);
|
||||
|
||||
@@ -624,12 +624,14 @@ impl ProjectItemRegistry {
|
||||
let project_path = project_path.clone();
|
||||
let is_file = project
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)
|
||||
.entry_for_path(dbg!(&project_path), cx)
|
||||
.is_some_and(|entry| entry.is_file());
|
||||
let entry_abs_path = project.read(cx).absolute_path(&project_path, cx);
|
||||
dbg!(&entry_abs_path);
|
||||
let is_local = project.read(cx).is_local();
|
||||
let project_item =
|
||||
<T::Item as project::ProjectItem>::try_open(project, &project_path, cx)?;
|
||||
dbg!();
|
||||
let project = project.clone();
|
||||
Some(window.spawn(cx, async move |cx| {
|
||||
match project_item.await.with_context(|| {
|
||||
@@ -693,6 +695,7 @@ impl ProjectItemRegistry {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<(Option<ProjectEntryId>, WorkspaceItemBuilder)>> {
|
||||
dbg!(self.build_project_item_for_path_fns.len());
|
||||
let Some(open_project_item) = self
|
||||
.build_project_item_for_path_fns
|
||||
.iter()
|
||||
|
||||
@@ -1350,7 +1350,7 @@ mod tests {
|
||||
|
||||
let (res_tx, res_rx) = oneshot::channel();
|
||||
req_tx.unbounded_send((req, res_tx)).unwrap();
|
||||
serde_json::to_string(&res_rx.await.unwrap()).unwrap()
|
||||
serde_json::to_string(&res_rx.await?).unwrap()
|
||||
}
|
||||
_ => {
|
||||
panic!("Unexpected path: {}", uri)
|
||||
|
||||
Reference in New Issue
Block a user