chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:08:36 +08:00
commit 93bdeb5e95
383 changed files with 83019 additions and 0 deletions
+69
View File
@@ -0,0 +1,69 @@
[package]
name = "lapce-proxy"
license = { workspace = true }
version = { workspace = true }
authors = { workspace = true }
edition = { workspace = true }
rust-version = { workspace = true }
[dependencies]
alacritty_terminal = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true }
crossbeam-channel = { workspace = true }
directories = { workspace = true }
flate2 = { workspace = true }
git2 = { workspace = true }
globset = { workspace = true }
indexmap = { workspace = true }
interprocess = { workspace = true }
notify = { workspace = true }
parking_lot = { workspace = true }
regex = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
tar = { workspace = true }
toml = { workspace = true }
tracing = { workspace = true }
tracing-log = { workspace = true }
url = { workspace = true }
zstd = { workspace = true }
lsp-types = { workspace = true }
psp-types = { workspace = true }
lapce-xi-rope = { workspace = true }
lapce-core = { workspace = true }
lapce-rpc = { workspace = true }
floem-editor-core = { workspace = true }
# proxy specific dependencies
dyn-clone = "1.0.16"
walkdir = "2.4.0"
jsonrpc-lite = "0.6.0"
polling = "3.5.0"
libc = "0.2"
# deleting files
trash = "3.0.6"
# search
ignore = "0.4"
grep-searcher = "0.1"
grep-matcher = "0.1"
grep-regex = "0.1"
# wasm
wasmtime = "14.0.0"
wasmtime-wasi = "14.0.0"
wasi-common = "14.0.0"
[dependencies.wasi-experimental-http-wasmtime]
git = "https://github.com/lapce/wasi-experimental-http"
# path = "../../wasi-experimental-http/crates/wasi-experimental-http-wasmtime"
[target.'cfg(target_os = "macos")'.dependencies.locale_config]
git = "https://github.com/lapce/locale_config.git"
branch = "lapce"
+5
View File
@@ -0,0 +1,5 @@
use lapce_proxy::mainloop;
fn main() {
mainloop();
}
+351
View File
@@ -0,0 +1,351 @@
use std::{
borrow::Cow,
ffi::OsString,
fs,
fs::File,
io::{Read, Write},
path::{Path, PathBuf},
time::SystemTime,
};
use anyhow::{Result, anyhow};
use floem_editor_core::buffer::rope_text::CharIndicesJoin;
use lapce_core::encoding::offset_utf8_to_utf16;
use lapce_rpc::buffer::BufferId;
use lapce_xi_rope::{RopeDelta, interval::IntervalBounds, rope::Rope};
use lsp_types::*;
#[derive(Clone)]
pub struct Buffer {
pub language_id: &'static str,
pub read_only: bool,
pub id: BufferId,
pub rope: Rope,
pub path: PathBuf,
pub rev: u64,
pub mod_time: Option<SystemTime>,
}
impl Buffer {
pub fn new(id: BufferId, path: PathBuf) -> Buffer {
let (s, read_only) = match load_file(&path) {
Ok(s) => (s, false),
Err(err) => {
use std::io::ErrorKind;
match err.downcast_ref::<std::io::Error>() {
Some(err) => match err.kind() {
ErrorKind::PermissionDenied => {
("Permission Denied".to_string(), true)
}
ErrorKind::NotFound => ("".to_string(), false),
ErrorKind::OutOfMemory => {
("File too big (out of memory)".to_string(), false)
}
_ => (format!("Not supported: {err}"), true),
},
None => (format!("Not supported: {err}"), true),
}
}
};
let rope = Rope::from(s);
let rev = u64::from(!rope.is_empty());
let language_id = language_id_from_path(&path).unwrap_or("");
let mod_time = get_mod_time(&path);
Buffer {
id,
rope,
read_only,
path,
language_id,
rev,
mod_time,
}
}
pub fn save(&mut self, rev: u64, create_parents: bool) -> Result<()> {
if self.read_only {
return Err(anyhow!("can't save to read only file"));
}
if self.rev != rev {
return Err(anyhow!("not the right rev"));
}
let bak_extension = self.path.extension().map_or_else(
|| OsString::from("bak"),
|ext| {
let mut ext = ext.to_os_string();
ext.push(".bak");
ext
},
);
let path = if self.path.is_symlink() {
self.path.canonicalize()?
} else {
self.path.clone()
};
let new_file = !path.exists();
let bak_file_path = &path.with_extension(bak_extension);
if !new_file {
fs::copy(&path, bak_file_path)?;
}
if create_parents {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
}
let mut f = fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&path)?;
for chunk in self.rope.iter_chunks(..self.rope.len()) {
f.write_all(chunk.as_bytes())?;
}
self.mod_time = get_mod_time(&path);
if !new_file {
fs::remove_file(bak_file_path)?;
}
Ok(())
}
pub fn update(
&mut self,
delta: &RopeDelta,
rev: u64,
) -> Option<TextDocumentContentChangeEvent> {
if self.rev + 1 != rev {
return None;
}
self.rev += 1;
let content_change = get_document_content_changes(delta, self);
self.rope = delta.apply(&self.rope);
Some(
content_change.unwrap_or_else(|| TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: self.get_document(),
}),
)
}
pub fn get_document(&self) -> String {
self.rope.to_string()
}
pub fn offset_of_line(&self, line: usize) -> usize {
self.rope.offset_of_line(line)
}
pub fn line_of_offset(&self, offset: usize) -> usize {
self.rope.line_of_offset(offset)
}
pub fn offset_to_line_col(&self, offset: usize) -> (usize, usize) {
let line = self.line_of_offset(offset);
(line, offset - self.offset_of_line(line))
}
/// Converts a UTF8 offset to a UTF16 LSP position
pub fn offset_to_position(&self, offset: usize) -> Position {
let (line, col) = self.offset_to_line_col(offset);
// Get the offset of line to make the conversion cheaper, rather than working
// from the very start of the document to `offset`
let line_offset = self.offset_of_line(line);
let utf16_col =
offset_utf8_to_utf16(self.char_indices_iter(line_offset..), col);
Position {
line: line as u32,
character: utf16_col as u32,
}
}
pub fn slice_to_cow<T: IntervalBounds>(&self, range: T) -> Cow<'_, str> {
self.rope.slice_to_cow(range)
}
pub fn line_to_cow(&self, line: usize) -> Cow<'_, str> {
self.rope
.slice_to_cow(self.offset_of_line(line)..self.offset_of_line(line + 1))
}
/// Iterate over (utf8_offset, char) values in the given range
/// This uses `iter_chunks` and so does not allocate, compared to `slice_to_cow` which can
pub fn char_indices_iter<T: IntervalBounds>(
&self,
range: T,
) -> impl Iterator<Item = (usize, char)> + '_ {
CharIndicesJoin::new(self.rope.iter_chunks(range).map(str::char_indices))
}
pub fn len(&self) -> usize {
self.rope.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
pub fn load_file(path: &Path) -> Result<String> {
read_path_to_string(path)
}
pub fn read_path_to_string<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();
let mut file = File::open(path)?;
// Read the file in as bytes
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
// Parse the file contents as utf8
let contents = String::from_utf8(buffer)?;
Ok(contents.to_string())
}
pub fn language_id_from_path(path: &Path) -> Option<&'static str> {
// recommended language_id values
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
Some(match path.extension() {
Some(ext) => {
match ext.to_str()? {
"C" | "H" => "cpp",
"M" => "objective-c",
// stop case-sensitive matching
ext => match ext.to_lowercase().as_str() {
"bat" => "bat",
// spellchecker:ignore-next-line
"clj" | "cljs" | "cljc" | "edn" => "clojure",
"coffee" => "coffeescript",
"c" | "h" => "c",
"cpp" | "hpp" | "cxx" | "hxx" | "c++" | "h++" | "cc" | "hh" => {
"cpp"
}
"cs" | "csx" => "csharp",
"css" => "css",
"d" | "di" | "dlang" => "dlang",
"diff" | "patch" => "diff",
"dart" => "dart",
"dockerfile" => "dockerfile",
"elm" => "elm",
"ex" | "exs" => "elixir",
"erl" | "hrl" => "erlang",
"fs" | "fsi" | "fsx" | "fsscript" => "fsharp",
"git-commit" | "git-rebase" => "git",
"go" => "go",
"groovy" | "gvy" | "gy" | "gsh" => "groovy",
"hbs" => "handlebars",
"htm" | "html" | "xhtml" => "html",
"ini" => "ini",
"java" | "class" => "java",
"js" => "javascript",
"jsx" => "javascriptreact",
"json" => "json",
"jl" => "julia",
"kt" => "kotlin",
"kts" => "kotlinbuildscript",
"less" => "less",
"lua" => "lua",
"makefile" | "gnumakefile" => "makefile",
"md" | "markdown" => "markdown",
"m" => "objective-c",
"mm" => "objective-cpp",
"plx" | "pl" | "pm" | "xs" | "t" | "pod" | "cgi" => "perl",
"p6" | "pm6" | "pod6" | "t6" | "raku" | "rakumod"
| "rakudoc" | "rakutest" => "perl6",
"php" | "phtml" | "pht" | "phps" => "php",
"proto" => "proto",
"ps1" | "ps1xml" | "psc1" | "psm1" | "psd1" | "pssc"
| "psrc" => "powershell",
"py" | "pyi" | "pyc" | "pyd" | "pyw" => "python",
"r" => "r",
"rb" => "ruby",
"rs" => "rust",
"scss" | "sass" => "scss",
"sc" | "scala" => "scala",
"sh" | "bash" | "zsh" => "shellscript",
"sql" => "sql",
"swift" => "swift",
"svelte" => "svelte",
"thrift" => "thrift",
"toml" => "toml",
"ts" => "typescript",
"tsx" => "typescriptreact",
"tex" => "tex",
"vb" => "vb",
"xml" | "csproj" => "xml",
"xsl" => "xsl",
"yml" | "yaml" => "yaml",
"zig" => "zig",
"vue" => "vue",
_ => return None,
},
}
}
// Handle paths without extension
#[allow(clippy::match_single_binding)]
None => match path.file_name()?.to_str()? {
// case-insensitive matching
filename => match filename.to_lowercase().as_str() {
"dockerfile" => "dockerfile",
"makefile" | "gnumakefile" => "makefile",
_ => return None,
},
},
})
}
fn get_document_content_changes(
delta: &RopeDelta,
buffer: &Buffer,
) -> Option<TextDocumentContentChangeEvent> {
let (interval, _) = delta.summary();
let (start, end) = interval.start_end();
// TODO: Handle more trivial cases like typing when there's a selection or transpose
if let Some(node) = delta.as_simple_insert() {
let (start, end) = interval.start_end();
let start = buffer.offset_to_position(start);
let end = buffer.offset_to_position(end);
Some(TextDocumentContentChangeEvent {
range: Some(Range { start, end }),
range_length: None,
text: String::from(node),
})
}
// Or a simple delete
else if delta.is_simple_delete() {
let end_position = buffer.offset_to_position(end);
let start = buffer.offset_to_position(start);
Some(TextDocumentContentChangeEvent {
range: Some(Range {
start,
end: end_position,
}),
range_length: None,
text: String::new(),
})
} else {
None
}
}
/// Returns the modification timestamp for the file at a given path,
/// if present.
pub fn get_mod_time<P: AsRef<Path>>(path: P) -> Option<SystemTime> {
File::open(path)
.and_then(|f| f.metadata())
.and_then(|meta| meta.modified())
.ok()
}
+219
View File
@@ -0,0 +1,219 @@
use std::path::PathBuf;
use anyhow::{Error, Result, anyhow};
use lapce_core::directory::Directory;
use lapce_rpc::{
RpcMessage,
file::{LineCol, PathObject},
proxy::{ProxyMessage, ProxyNotification},
};
#[derive(Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum PathObjectType {
#[default]
Directory,
File,
}
pub fn parse_file_line_column(path: &str) -> Result<PathObject, Error> {
if let Ok(path) = PathBuf::from(path).canonicalize() {
return Ok(PathObject {
is_dir: path.is_dir(),
path,
linecol: None,
});
}
let pwd = std::env::current_dir().unwrap_or_default();
let mut splits = path.rsplit(':').peekable();
let (path, linecol) = if let Some(first_rhs) =
splits.peek().and_then(|s| s.parse::<usize>().ok())
{
splits.next();
if let Some(second_rhs) = splits.peek().and_then(|s| s.parse::<usize>().ok())
{
splits.next();
let remaining: Vec<&str> = splits.rev().collect();
let path = remaining.join(":");
let path = PathBuf::from(path);
let path = if let Ok(path) = path.canonicalize() {
path
} else {
pwd.join(&path)
};
(
path,
Some(LineCol {
line: second_rhs,
column: first_rhs,
}),
)
} else {
let remaining: Vec<&str> = splits.rev().collect();
let path = remaining.join(":");
let path = PathBuf::from(path);
let path = if let Ok(path) = path.canonicalize() {
path
} else {
pwd.join(&path)
};
(
path,
Some(LineCol {
line: first_rhs,
column: 1,
}),
)
}
} else {
(pwd.join(path), None)
};
Ok(PathObject {
is_dir: path.is_dir(),
path,
linecol,
})
}
pub fn try_open_in_existing_process(paths: &[PathObject]) -> Result<()> {
let local_socket = Directory::local_socket()
.ok_or_else(|| anyhow!("can't get local socket folder"))?;
let mut socket =
interprocess::local_socket::LocalSocketStream::connect(local_socket)?;
let msg: ProxyMessage = RpcMessage::Notification(ProxyNotification::OpenPaths {
paths: paths.to_vec(),
});
lapce_rpc::stdio::write_msg(&mut socket, msg)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::{env, path::PathBuf};
use super::parse_file_line_column;
use crate::cli::PathObject;
#[test]
#[cfg(windows)]
fn test_absolute_path() {
assert_eq!(
parse_file_line_column("C:\\Cargo.toml:55").unwrap(),
PathObject::new(PathBuf::from("C:\\Cargo.toml"), false, 55, 1),
);
}
#[test]
#[cfg(windows)]
fn test_relative_path() {
assert_eq!(
parse_file_line_column(".\\..\\Cargo.toml:55").unwrap(),
PathObject::new(
PathBuf::from(".\\..\\Cargo.toml").canonicalize().unwrap(),
false,
55,
1
),
);
}
#[test]
#[cfg(windows)]
fn test_directory_looking_like_file() {
assert_eq!(
parse_file_line_column(".\\Cargo.toml\\").unwrap(),
PathObject::from_path(
env::current_dir().unwrap().join("Cargo.toml"),
false
),
);
}
#[test]
#[cfg(unix)]
fn test_absolute_path() {
assert_eq!(
parse_file_line_column("/tmp/Cargo.toml:55").unwrap(),
PathObject::new(PathBuf::from("/tmp/Cargo.toml"), false, 55, 1),
);
}
#[test]
#[cfg(unix)]
fn test_relative_path() {
assert_eq!(
parse_file_line_column("./../Cargo.toml").unwrap(),
PathObject::from_path(
PathBuf::from("./../Cargo.toml").canonicalize().unwrap(),
false,
),
);
}
#[test]
#[cfg(unix)]
fn test_directory_looking_like_file() {
assert_eq!(
parse_file_line_column("./Cargo.toml/").unwrap(),
PathObject::from_path(
env::current_dir().unwrap().join("Cargo.toml"),
false
),
);
}
#[test]
fn test_current_dir() {
assert_eq!(
parse_file_line_column(".").unwrap(),
PathObject::from_path(
env::current_dir().unwrap().canonicalize().unwrap(),
true
),
);
}
#[test]
fn test_relative_path_with_line() {
assert_eq!(
parse_file_line_column("Cargo.toml:55").unwrap(),
PathObject::new(
PathBuf::from("Cargo.toml").canonicalize().unwrap(),
false,
55,
1
),
);
}
#[test]
fn test_relative_path_with_linecol() {
assert_eq!(
parse_file_line_column("Cargo.toml:55:3").unwrap(),
PathObject::new(
PathBuf::from("Cargo.toml").canonicalize().unwrap(),
false,
55,
3
),
);
}
#[test]
fn test_relative_path_with_none() {
assert_eq!(
parse_file_line_column("Cargo.toml:12:623:352").unwrap(),
PathObject::new(
env::current_dir()
.unwrap()
.join(PathBuf::from("Cargo.toml:12")),
false,
623,
352
),
);
}
}
File diff suppressed because it is too large Load Diff
+215
View File
@@ -0,0 +1,215 @@
#![allow(clippy::manual_clamp)]
pub mod buffer;
pub mod cli;
pub mod dispatch;
pub mod plugin;
pub mod terminal;
pub mod watcher;
use std::{
io::{BufReader, stdin, stdout},
process::exit,
sync::Arc,
thread,
};
use anyhow::{Result, anyhow};
use clap::Parser;
use dispatch::Dispatcher;
use lapce_core::{directory::Directory, meta};
use lapce_rpc::{
RpcMessage,
core::{CoreRpc, CoreRpcHandler},
file::PathObject,
proxy::{ProxyMessage, ProxyNotification, ProxyRpcHandler},
stdio::stdio_transport,
};
use tracing::error;
#[derive(Parser)]
#[clap(name = "Lapce-proxy")]
#[clap(version = meta::VERSION)]
struct Cli {
#[clap(short, long, action, hide = true)]
proxy: bool,
/// Paths to file(s) and/or folder(s) to open.
/// When path is a file (that exists or not),
/// it accepts `path:line:column` syntax
/// to specify line and column at which it should open the file
#[clap(value_parser = cli::parse_file_line_column)]
#[clap(value_hint = clap::ValueHint::AnyPath)]
paths: Vec<PathObject>,
}
pub fn mainloop() {
let cli = Cli::parse();
if !cli.proxy {
if let Err(e) = cli::try_open_in_existing_process(&cli.paths) {
error!("failed to open path(s): {e}");
};
exit(1);
}
let core_rpc = CoreRpcHandler::new();
let proxy_rpc = ProxyRpcHandler::new();
let mut dispatcher = Dispatcher::new(core_rpc.clone(), proxy_rpc.clone());
let (writer_tx, writer_rx) = crossbeam_channel::unbounded();
let (reader_tx, reader_rx) = crossbeam_channel::unbounded();
stdio_transport(stdout(), writer_rx, BufReader::new(stdin()), reader_tx);
let local_core_rpc = core_rpc.clone();
let local_writer_tx = writer_tx.clone();
thread::spawn(move || {
for msg in local_core_rpc.rx() {
match msg {
CoreRpc::Request(id, rpc) => {
if let Err(err) =
local_writer_tx.send(RpcMessage::Request(id, rpc))
{
tracing::error!("{:?}", err);
}
}
CoreRpc::Notification(rpc) => {
if let Err(err) =
local_writer_tx.send(RpcMessage::Notification(rpc))
{
tracing::error!("{:?}", err);
}
}
CoreRpc::Shutdown => {
return;
}
}
}
});
let local_proxy_rpc = proxy_rpc.clone();
let writer_tx = Arc::new(writer_tx);
thread::spawn(move || {
for msg in reader_rx {
match msg {
RpcMessage::Request(id, req) => {
let writer_tx = writer_tx.clone();
local_proxy_rpc.request_async(req, move |result| match result {
Ok(resp) => {
if let Err(err) =
writer_tx.send(RpcMessage::Response(id, resp))
{
tracing::error!("{:?}", err);
}
}
Err(e) => {
if let Err(err) =
writer_tx.send(RpcMessage::Error(id, e))
{
tracing::error!("{:?}", err);
}
}
});
}
RpcMessage::Notification(n) => {
local_proxy_rpc.notification(n);
}
RpcMessage::Response(id, resp) => {
core_rpc.handle_response(id, Ok(resp));
}
RpcMessage::Error(id, err) => {
core_rpc.handle_response(id, Err(err));
}
}
}
local_proxy_rpc.shutdown();
});
let local_proxy_rpc = proxy_rpc.clone();
std::thread::spawn(move || {
if let Err(err) = listen_local_socket(local_proxy_rpc) {
tracing::error!("{:?}", err);
}
});
if let Err(err) = register_lapce_path() {
tracing::error!("{:?}", err);
}
proxy_rpc.mainloop(&mut dispatcher);
}
pub fn register_lapce_path() -> Result<()> {
let exedir = std::env::current_exe()?
.parent()
.ok_or(anyhow!("can't get parent dir of exe"))?
.canonicalize()?;
let current_path = std::env::var("PATH")?;
let paths = std::env::split_paths(&current_path);
for path in paths {
if exedir == path.canonicalize()? {
return Ok(());
}
}
let paths = std::env::split_paths(&current_path);
let paths = std::env::join_paths(std::iter::once(exedir).chain(paths))?;
unsafe {
std::env::set_var("PATH", paths);
}
Ok(())
}
fn listen_local_socket(proxy_rpc: ProxyRpcHandler) -> Result<()> {
let local_socket = Directory::local_socket()
.ok_or_else(|| anyhow!("can't get local socket folder"))?;
if let Err(err) = std::fs::remove_file(&local_socket) {
tracing::error!("{:?}", err);
}
let socket =
interprocess::local_socket::LocalSocketListener::bind(local_socket)?;
for stream in socket.incoming().flatten() {
let mut reader = BufReader::new(stream);
let proxy_rpc = proxy_rpc.clone();
thread::spawn(move || -> Result<()> {
loop {
let msg: Option<ProxyMessage> =
lapce_rpc::stdio::read_msg(&mut reader)?;
if let Some(RpcMessage::Notification(
ProxyNotification::OpenPaths { paths },
)) = msg
{
proxy_rpc.notification(ProxyNotification::OpenPaths { paths });
}
}
});
}
Ok(())
}
pub fn get_url<T: reqwest::IntoUrl + Clone>(
url: T,
user_agent: Option<&str>,
) -> Result<reqwest::blocking::Response> {
let mut builder = if let Ok(proxy) = std::env::var("https_proxy") {
let proxy = reqwest::Proxy::all(proxy)?;
reqwest::blocking::Client::builder()
.proxy(proxy)
.timeout(std::time::Duration::from_secs(10))
} else {
reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_secs(10))
};
if let Some(user_agent) = user_agent {
builder = builder.user_agent(user_agent);
}
let client = builder.build()?;
let mut try_time = 0;
loop {
let rs = client.get(url.clone()).send();
if rs.is_ok() || try_time > 3 {
return Ok(rs?);
} else {
try_time += 1;
}
}
}
+756
View File
@@ -0,0 +1,756 @@
use std::{
borrow::Cow,
collections::HashMap,
path::PathBuf,
sync::{
Arc,
atomic::{AtomicUsize, Ordering},
},
thread,
};
use lapce_rpc::{
RpcError,
dap_types::{self, DapId, DapServer, SetBreakpointsResponse},
plugin::{PluginId, VoltID, VoltInfo, VoltMetadata},
proxy::ProxyResponse,
style::LineStyle,
};
use lapce_xi_rope::{Rope, RopeDelta};
use lsp_types::{
DidOpenTextDocumentParams, MessageType, SemanticTokens, ShowMessageParams,
TextDocumentIdentifier, TextDocumentItem, VersionedTextDocumentIdentifier,
notification::DidOpenTextDocument, request::Request,
};
use parking_lot::Mutex;
use psp_types::Notification;
use serde_json::Value;
use super::{
PluginCatalogNotification, PluginCatalogRpcHandler,
dap::{DapClient, DapRpcHandler, DebuggerData},
psp::{CloneableCallback, PluginServerRpc, PluginServerRpcHandler, RpcCallback},
wasi::{load_all_volts, start_volt},
};
use crate::plugin::{
install_volt, psp::PluginHandlerNotification, wasi::enable_volt,
};
pub struct PluginCatalog {
workspace: Option<PathBuf>,
plugin_rpc: PluginCatalogRpcHandler,
plugins: HashMap<PluginId, PluginServerRpcHandler>,
daps: HashMap<DapId, DapRpcHandler>,
debuggers: HashMap<String, DebuggerData>,
plugin_configurations: HashMap<String, HashMap<String, serde_json::Value>>,
unactivated_volts: HashMap<VoltID, VoltMetadata>,
open_files: HashMap<PathBuf, String>,
}
impl PluginCatalog {
pub fn new(
workspace: Option<PathBuf>,
disabled_volts: Vec<VoltID>,
extra_plugin_paths: Vec<PathBuf>,
plugin_configurations: HashMap<String, HashMap<String, serde_json::Value>>,
plugin_rpc: PluginCatalogRpcHandler,
) -> Self {
let plugin = Self {
workspace,
plugin_rpc: plugin_rpc.clone(),
plugin_configurations,
plugins: HashMap::new(),
daps: HashMap::new(),
debuggers: HashMap::new(),
unactivated_volts: HashMap::new(),
open_files: HashMap::new(),
};
thread::spawn(move || {
load_all_volts(plugin_rpc, &extra_plugin_paths, disabled_volts);
});
plugin
}
#[allow(clippy::too_many_arguments)]
pub fn handle_server_request(
&mut self,
plugin_id: Option<PluginId>,
request_sent: Option<Arc<AtomicUsize>>,
method: Cow<'static, str>,
params: Value,
language_id: Option<String>,
path: Option<PathBuf>,
check: bool,
f: Box<dyn CloneableCallback<Value, RpcError>>,
) {
if let Some(plugin_id) = plugin_id {
if let Some(plugin) = self.plugins.get(&plugin_id) {
plugin.server_request_async(
method,
params,
language_id,
path,
check,
move |result| {
f(plugin_id, result);
},
);
} else {
f(
plugin_id,
Err(RpcError {
code: 0,
message: "plugin doesn't exist".to_string(),
}),
);
}
return;
}
if let Some(request_sent) = request_sent {
// if there are no plugins installed the callback of the client is not called
// so check if plugins list is empty
if self.plugins.is_empty() {
// Add a request
request_sent.fetch_add(1, Ordering::Relaxed);
// make a direct callback with an "error"
f(
lapce_rpc::plugin::PluginId(0),
Err(RpcError {
code: 0,
message: "no available plugin could make a callback, because the plugins list is empty".to_string(),
}),
);
return;
} else {
request_sent.fetch_add(self.plugins.len(), Ordering::Relaxed);
}
}
for (plugin_id, plugin) in self.plugins.iter() {
let f = dyn_clone::clone_box(&*f);
let plugin_id = *plugin_id;
plugin.server_request_async(
method.clone(),
params.clone(),
language_id.clone(),
path.clone(),
check,
move |result| {
f(plugin_id, result);
},
);
}
}
#[allow(clippy::too_many_arguments)]
pub fn handle_server_notification(
&mut self,
plugin_id: Option<PluginId>,
method: impl Into<Cow<'static, str>>,
params: Value,
language_id: Option<String>,
path: Option<PathBuf>,
check: bool,
) {
if let Some(plugin_id) = plugin_id {
if let Some(plugin) = self.plugins.get(&plugin_id) {
plugin.server_notification(method, params, language_id, path, check);
}
return;
}
// Otherwise send it to all plugins
let method = method.into();
for (_, plugin) in self.plugins.iter() {
plugin.server_notification(
method.clone(),
params.clone(),
language_id.clone(),
path.clone(),
check,
);
}
}
pub fn shutdown_volt(
&mut self,
volt: VoltInfo,
f: Box<dyn CloneableCallback<Value, RpcError>>,
) {
let id = volt.id();
for (plugin_id, plugin) in self.plugins.iter() {
if plugin.volt_id == id {
let f = dyn_clone::clone_box(&*f);
let plugin_id = *plugin_id;
plugin.server_request_async(
lsp_types::request::Shutdown::METHOD,
Value::Null,
None,
None,
false,
move |result| {
f(plugin_id, result);
},
);
plugin.shutdown();
}
}
}
fn start_unactivated_volts(&mut self, to_be_activated: Vec<VoltID>) {
for id in to_be_activated.iter() {
let workspace = self.workspace.clone();
if let Some(meta) = self.unactivated_volts.remove(id) {
let configurations =
self.plugin_configurations.get(&meta.name).cloned();
tracing::debug!("{:?} {:?}", id, configurations);
let plugin_rpc = self.plugin_rpc.clone();
thread::spawn(move || {
if let Err(err) =
start_volt(workspace, configurations, plugin_rpc, meta)
{
tracing::error!("{:?}", err);
}
});
}
}
}
fn check_unactivated_volts(&mut self) {
let to_be_activated: Vec<VoltID> = self
.unactivated_volts
.iter()
.filter_map(|(id, meta)| {
let contains = meta
.activation
.as_ref()
.and_then(|a| a.language.as_ref())
.map(|l| {
self.open_files
.iter()
.any(|(_, language_id)| l.contains(language_id))
})
.unwrap_or(false);
if contains {
return Some(id.clone());
}
if let Some(workspace) = self.workspace.as_ref() {
if let Some(globs) = meta
.activation
.as_ref()
.and_then(|a| a.workspace_contains.as_ref())
{
let mut builder = globset::GlobSetBuilder::new();
for glob in globs {
match globset::Glob::new(glob) {
Ok(glob) => {
builder.add(glob);
}
Err(err) => {
tracing::error!("{:?}", err);
}
}
}
match builder.build() {
Ok(matcher) => {
if !matcher.is_empty() {
for entry in walkdir::WalkDir::new(workspace)
.into_iter()
.flatten()
{
if matcher.is_match(entry.path()) {
return Some(id.clone());
}
}
}
}
Err(err) => {
tracing::error!("{:?}", err);
}
}
}
}
None
})
.collect();
self.start_unactivated_volts(to_be_activated);
}
pub fn handle_did_open_text_document(&mut self, document: TextDocumentItem) {
match document.uri.to_file_path() {
Ok(path) => {
self.open_files.insert(path, document.language_id.clone());
}
Err(err) => {
tracing::error!("{:?}", err);
}
}
let to_be_activated: Vec<VoltID> = self
.unactivated_volts
.iter()
.filter_map(|(id, meta)| {
let contains = meta
.activation
.as_ref()
.and_then(|a| a.language.as_ref())
.map(|l| l.contains(&document.language_id))?;
if contains { Some(id.clone()) } else { None }
})
.collect();
self.start_unactivated_volts(to_be_activated);
let path = document.uri.to_file_path().ok();
for (_, plugin) in self.plugins.iter() {
plugin.server_notification(
DidOpenTextDocument::METHOD,
DidOpenTextDocumentParams {
text_document: document.clone(),
},
Some(document.language_id.clone()),
path.clone(),
true,
);
}
}
pub fn handle_did_save_text_document(
&mut self,
language_id: String,
path: PathBuf,
text_document: TextDocumentIdentifier,
text: Rope,
) {
for (_, plugin) in self.plugins.iter() {
plugin.handle_rpc(PluginServerRpc::DidSaveTextDocument {
language_id: language_id.clone(),
path: path.clone(),
text_document: text_document.clone(),
text: text.clone(),
});
}
}
pub fn handle_did_change_text_document(
&mut self,
language_id: String,
document: VersionedTextDocumentIdentifier,
delta: RopeDelta,
text: Rope,
new_text: Rope,
) {
let change = Arc::new(Mutex::new((None, None)));
for (_, plugin) in self.plugins.iter() {
plugin.handle_rpc(PluginServerRpc::DidChangeTextDocument {
language_id: language_id.clone(),
document: document.clone(),
delta: delta.clone(),
text: text.clone(),
new_text: new_text.clone(),
change: change.clone(),
});
}
}
pub fn format_semantic_tokens(
&self,
plugin_id: PluginId,
tokens: SemanticTokens,
text: Rope,
f: Box<dyn RpcCallback<Vec<LineStyle>, RpcError>>,
) {
if let Some(plugin) = self.plugins.get(&plugin_id) {
plugin.handle_rpc(PluginServerRpc::FormatSemanticTokens {
tokens,
text,
f,
});
} else {
f.call(Err(RpcError {
code: 0,
message: "plugin doesn't exist".to_string(),
}));
}
}
pub fn dap_variable(
&self,
dap_id: DapId,
reference: usize,
f: Box<dyn RpcCallback<Vec<dap_types::Variable>, RpcError>>,
) {
if let Some(dap) = self.daps.get(&dap_id) {
dap.variables_async(
reference,
|result: Result<dap_types::VariablesResponse, RpcError>| {
f.call(result.map(|resp| resp.variables))
},
);
} else {
f.call(Err(RpcError {
code: 0,
message: "plugin doesn't exist".to_string(),
}));
}
}
pub fn dap_get_scopes(
&self,
dap_id: DapId,
frame_id: usize,
f: Box<
dyn RpcCallback<
Vec<(dap_types::Scope, Vec<dap_types::Variable>)>,
RpcError,
>,
>,
) {
if let Some(dap) = self.daps.get(&dap_id) {
let local_dap = dap.clone();
dap.scopes_async(
frame_id,
move |result: Result<dap_types::ScopesResponse, RpcError>| {
match result {
Ok(resp) => {
let scopes = resp.scopes.clone();
if let Some(scope) = resp.scopes.first() {
let scope = scope.to_owned();
thread::spawn(move || {
local_dap.variables_async(
scope.variables_reference,
move |result: Result<
dap_types::VariablesResponse,
RpcError,
>| {
let resp: Vec<(
dap_types::Scope,
Vec<dap_types::Variable>,
)> = scopes
.iter()
.enumerate()
.map(|(index, s)| {
(
s.clone(),
if index == 0 {
result
.as_ref()
.map(|resp| {
resp.variables
.clone()
})
.unwrap_or_default()
} else {
Vec::new()
},
)
})
.collect();
f.call(Ok(resp));
},
);
});
} else {
f.call(Ok(Vec::new()));
}
}
Err(e) => {
f.call(Err(e));
}
}
},
);
} else {
f.call(Err(RpcError {
code: 0,
message: "plugin doesn't exist".to_string(),
}));
}
}
pub fn handle_notification(&mut self, notification: PluginCatalogNotification) {
use PluginCatalogNotification::*;
match notification {
UnactivatedVolts(volts) => {
tracing::debug!("UnactivatedVolts {:?}", volts);
for volt in volts {
let id = volt.id();
self.unactivated_volts.insert(id, volt);
}
self.check_unactivated_volts();
}
UpdatePluginConfigs(configs) => {
tracing::debug!("UpdatePluginConfigs {:?}", configs);
self.plugin_configurations = configs;
}
PluginServerLoaded(plugin) => {
// TODO: check if the server has did open registered
match self.plugin_rpc.proxy_rpc.get_open_files_content() {
Ok(ProxyResponse::GetOpenFilesContentResponse { items }) => {
for item in items {
let language_id = Some(item.language_id.clone());
let path = item.uri.to_file_path().ok();
plugin.server_notification(
DidOpenTextDocument::METHOD,
DidOpenTextDocumentParams {
text_document: item,
},
language_id,
path,
true,
);
}
}
Ok(_) => {}
Err(err) => {
tracing::error!("{:?}", err);
}
}
let plugin_id = plugin.plugin_id;
let spawned_by = plugin.spawned_by;
self.plugins.insert(plugin.plugin_id, plugin);
if let Some(spawned_by) = spawned_by {
if let Some(plugin) = self.plugins.get(&spawned_by) {
plugin.handle_rpc(PluginServerRpc::Handler(
PluginHandlerNotification::SpawnedPluginLoaded {
plugin_id,
},
));
}
}
}
InstallVolt(volt) => {
tracing::debug!("InstallVolt {:?}", volt);
let workspace = self.workspace.clone();
let configurations =
self.plugin_configurations.get(&volt.name).cloned();
let catalog_rpc = self.plugin_rpc.clone();
catalog_rpc.stop_volt(volt.clone());
thread::spawn(move || {
if let Err(err) =
install_volt(catalog_rpc, workspace, configurations, volt)
{
tracing::error!("{:?}", err);
}
});
}
ReloadVolt(volt) => {
tracing::debug!("ReloadVolt {:?}", volt);
let volt_id = volt.id();
let ids: Vec<PluginId> = self.plugins.keys().cloned().collect();
for id in ids {
if self.plugins.get(&id).unwrap().volt_id == volt_id {
let plugin = self.plugins.remove(&id).unwrap();
plugin.shutdown();
}
}
if let Err(err) = self.plugin_rpc.unactivated_volts(vec![volt]) {
tracing::error!("{:?}", err);
}
}
StopVolt(volt) => {
tracing::debug!("StopVolt {:?}", volt);
let volt_id = volt.id();
let ids: Vec<PluginId> = self.plugins.keys().cloned().collect();
for id in ids {
if self.plugins.get(&id).unwrap().volt_id == volt_id {
let plugin = self.plugins.remove(&id).unwrap();
plugin.shutdown();
}
}
}
EnableVolt(volt) => {
tracing::debug!("EnableVolt {:?}", volt);
let volt_id = volt.id();
for (_, volt) in self.plugins.iter() {
if volt.volt_id == volt_id {
return;
}
}
let plugin_rpc = self.plugin_rpc.clone();
thread::spawn(move || {
if let Err(err) = enable_volt(plugin_rpc, volt) {
tracing::error!("{:?}", err);
}
});
}
DapLoaded(dap_rpc) => {
self.daps.insert(dap_rpc.dap_id, dap_rpc);
}
DapDisconnected(dap_id) => {
self.daps.remove(&dap_id);
}
DapStart {
config,
breakpoints,
} => {
let workspace = self.workspace.clone();
let plugin_rpc = self.plugin_rpc.clone();
if let Some(debugger) = config
.ty
.as_ref()
.and_then(|ty| self.debuggers.get(ty).cloned())
{
thread::spawn(move || {
match DapClient::start(
DapServer {
program: debugger.program,
args: debugger.args.unwrap_or_default(),
cwd: workspace,
},
config.clone(),
breakpoints,
plugin_rpc.clone(),
) {
Ok(dap_rpc) => {
if let Err(err) =
plugin_rpc.dap_loaded(dap_rpc.clone())
{
tracing::error!("{:?}", err);
}
if let Err(err) = dap_rpc.launch(&config) {
tracing::error!("{:?}", err);
}
}
Err(err) => {
tracing::error!("{:?}", err);
}
}
});
} else {
self.plugin_rpc.core_rpc.show_message(
"debug fail".to_owned(),
ShowMessageParams {
typ: MessageType::ERROR,
message: "Debugger not found. Please install the appropriate plugin.".to_owned(),
},
)
}
}
DapProcessId {
dap_id,
process_id,
term_id,
} => {
if let Some(dap) = self.daps.get(&dap_id) {
if let Err(err) =
dap.termain_process_tx.send((term_id, process_id))
{
tracing::error!("{:?}", err);
}
}
}
DapContinue { dap_id, thread_id } => {
if let Some(dap) = self.daps.get(&dap_id).cloned() {
let plugin_rpc = self.plugin_rpc.clone();
thread::spawn(move || {
if dap.continue_thread(thread_id).is_ok() {
plugin_rpc.core_rpc.dap_continued(dap_id);
}
});
}
}
DapPause { dap_id, thread_id } => {
if let Some(dap) = self.daps.get(&dap_id).cloned() {
thread::spawn(move || {
if let Err(err) = dap.pause_thread(thread_id) {
tracing::error!("{:?}", err);
}
});
}
}
DapStepOver { dap_id, thread_id } => {
if let Some(dap) = self.daps.get(&dap_id).cloned() {
dap.next(thread_id);
}
}
DapStepInto { dap_id, thread_id } => {
if let Some(dap) = self.daps.get(&dap_id).cloned() {
dap.step_in(thread_id);
}
}
DapStepOut { dap_id, thread_id } => {
if let Some(dap) = self.daps.get(&dap_id).cloned() {
dap.step_out(thread_id);
}
}
DapStop { dap_id } => {
if let Some(dap) = self.daps.get(&dap_id) {
dap.stop();
}
}
DapDisconnect { dap_id } => {
if let Some(dap) = self.daps.get(&dap_id).cloned() {
thread::spawn(move || {
if let Err(err) = dap.disconnect() {
tracing::error!("{:?}", err);
}
});
}
}
DapRestart {
dap_id,
breakpoints,
} => {
if let Some(dap) = self.daps.get(&dap_id) {
dap.restart(breakpoints);
}
}
DapSetBreakpoints {
dap_id,
path,
breakpoints,
} => {
if let Some(dap) = self.daps.get(&dap_id) {
let core_rpc = self.plugin_rpc.core_rpc.clone();
dap.set_breakpoints_async(
path.clone(),
breakpoints,
move |result: Result<SetBreakpointsResponse, RpcError>| {
match result {
Ok(resp) => {
core_rpc.dap_breakpoints_resp(
dap_id,
path,
resp.breakpoints.unwrap_or_default(),
);
}
Err(err) => {
tracing::error!("{:?}", err);
}
}
},
);
}
}
RegisterDebuggerType {
debugger_type,
program,
args,
} => {
self.debuggers.insert(
debugger_type.clone(),
DebuggerData {
debugger_type,
program,
args,
},
);
}
Shutdown => {
for (_, plugin) in self.plugins.iter() {
plugin.shutdown();
}
}
}
}
}
+824
View File
@@ -0,0 +1,824 @@
use std::{
collections::HashMap,
io::{BufReader, BufWriter, Write},
path::PathBuf,
process::{Child, Command, Stdio},
sync::{
Arc,
atomic::{AtomicU64, Ordering},
},
thread,
};
use anyhow::{Result, anyhow};
use crossbeam_channel::{Receiver, Sender};
use lapce_rpc::{
RpcError,
dap_types::{
self, ConfigurationDone, Continue, ContinueArguments, ContinueResponse,
DapEvent, DapId, DapPayload, DapRequest, DapResponse, DapServer,
DebuggerCapabilities, Disconnect, Initialize, Launch, Next, NextArguments,
Pause, PauseArguments, Request, RunDebugConfig, RunInTerminal,
RunInTerminalArguments, RunInTerminalResponse, Scope, Scopes,
ScopesArguments, ScopesResponse, SetBreakpoints, SetBreakpointsArguments,
SetBreakpointsResponse, Source, SourceBreakpoint, StackTrace,
StackTraceArguments, StackTraceResponse, StepIn, StepInArguments, StepOut,
StepOutArguments, Terminate, ThreadId, Threads, ThreadsResponse, Variable,
Variables, VariablesArguments, VariablesResponse,
},
terminal::TermId,
};
use parking_lot::Mutex;
use serde_json::Value;
use super::{
PluginCatalogRpcHandler,
psp::{ResponseHandler, RpcCallback},
};
pub struct DapClient {
plugin_rpc: PluginCatalogRpcHandler,
pub(crate) dap_rpc: DapRpcHandler,
dap_server: DapServer,
config: RunDebugConfig,
breakpoints: HashMap<PathBuf, Vec<SourceBreakpoint>>,
term_id: Option<TermId>,
capabilities: Option<DebuggerCapabilities>,
terminated: bool,
disconnected: bool,
restarted: bool,
}
impl DapClient {
pub fn new(
dap_server: DapServer,
config: RunDebugConfig,
breakpoints: HashMap<PathBuf, Vec<SourceBreakpoint>>,
plugin_rpc: PluginCatalogRpcHandler,
) -> Result<Self> {
let dap_rpc = DapRpcHandler::new(config.dap_id);
Ok(Self {
plugin_rpc,
dap_server,
config,
dap_rpc,
breakpoints,
term_id: None,
capabilities: None,
terminated: false,
disconnected: false,
restarted: false,
})
}
pub fn start(
dap_server: DapServer,
config: RunDebugConfig,
breakpoints: HashMap<PathBuf, Vec<SourceBreakpoint>>,
plugin_rpc: PluginCatalogRpcHandler,
) -> Result<DapRpcHandler> {
let mut dap = Self::new(dap_server, config, breakpoints, plugin_rpc)?;
dap.start_process()?;
let dap_rpc = dap.dap_rpc.clone();
dap.initialize()?;
{
let dap_rpc = dap_rpc.clone();
thread::spawn(move || {
dap_rpc.mainloop(&mut dap);
});
}
Ok(dap_rpc)
}
fn start_process(&self) -> Result<()> {
let program = self.dap_server.program.clone();
let mut process = Self::process(
&program,
&self.dap_server.args,
self.dap_server.cwd.as_ref(),
)?;
let stdin = process.stdin.take().unwrap();
let stdout = process.stdout.take().unwrap();
// let stderr = process.stderr.take().unwrap();
let dap_rpc = self.dap_rpc.clone();
let io_rx = self.dap_rpc.io_rx.clone();
let io_tx = self.dap_rpc.io_tx.clone();
let mut writer = Box::new(BufWriter::new(stdin));
thread::spawn(move || -> Result<()> {
for msg in io_rx {
if let Ok(msg) = serde_json::to_string(&msg) {
tracing::debug!("write to dap server: {}", msg);
let msg =
format!("Content-Length: {}\r\n\r\n{}", msg.len(), msg);
writer.write_all(msg.as_bytes())?;
writer.flush()?;
}
}
tracing::debug!("thread(write to dap) exited");
Ok(())
});
{
let plugin_rpc = self.plugin_rpc.clone();
thread::spawn(move || {
let mut reader = Box::new(BufReader::new(stdout));
loop {
match crate::plugin::lsp::read_message(&mut reader) {
Ok(message_str) => {
tracing::debug!("read from dap server: {}", message_str);
dap_rpc.handle_server_message(&message_str);
}
Err(_err) => {
if let Err(err) = io_tx
.send(DapPayload::Event(DapEvent::Initialized(None)))
{
tracing::error!("{:?}", err);
}
plugin_rpc.core_rpc.log(
lapce_rpc::core::LogLevel::Error,
format!("dap server {program} stopped!"),
None,
);
dap_rpc.disconnected();
tracing::debug!("thread(read from dap) exited");
return;
}
};
}
});
}
Ok(())
}
fn process(
server: &str,
args: &[String],
cwd: Option<&PathBuf>,
) -> Result<Child> {
let mut process = Command::new(server);
if let Some(cwd) = cwd {
process.current_dir(cwd);
}
process.args(args);
// CREATE_NO_WINDOW
// (https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags)
// TODO: We set this because
#[cfg(target_os = "windows")]
std::os::windows::process::CommandExt::creation_flags(
&mut process,
0x08000000,
);
let child = process
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
Ok(child)
}
fn handle_host_request(&mut self, req: &DapRequest) -> Result<Value> {
match req.command.as_str() {
RunInTerminal::COMMAND => {
let value = req
.arguments
.as_ref()
.ok_or_else(|| anyhow!("no arguments"))?;
let args: RunInTerminalArguments =
serde_json::from_value(value.clone())?;
let mut config = self.config.clone();
config.debug_command = Some(args.args);
self.plugin_rpc.core_rpc.run_in_terminal(config);
let (term_id, process_id) =
self.dap_rpc.termain_process_rx.recv()?;
self.term_id = Some(term_id);
let resp = RunInTerminalResponse {
process_id,
shell_process_id: None,
};
let resp = serde_json::to_value(resp)?;
Ok(resp)
}
_ => Err(anyhow!("not implemented")),
}
}
fn handle_host_event(&mut self, event: &DapEvent) -> Result<()> {
match event {
DapEvent::Initialized(_) => {
for (path, breakpoints) in self.breakpoints.clone().into_iter() {
match self.dap_rpc.set_breakpoints(path.clone(), breakpoints) {
Ok(breakpoints) => {
self.plugin_rpc.core_rpc.dap_breakpoints_resp(
self.config.dap_id,
path,
breakpoints.breakpoints.unwrap_or_default(),
);
}
Err(err) => {
tracing::error!("{:?}", err);
}
}
}
// send dap configurations here
self.dap_rpc.request_async::<ConfigurationDone>((), |rs| {
if let Err(e) = rs {
tracing::error!("request ConfigurationDone: {:?}", e)
}
});
}
DapEvent::Stopped(stopped) => {
let all_threads_stopped =
stopped.all_threads_stopped.unwrap_or_default();
let mut stack_frames = HashMap::new();
if all_threads_stopped {
if let Ok(response) = self.dap_rpc.threads() {
for thread in response.threads {
if let Ok(frames) = self.dap_rpc.stack_trace(thread.id) {
stack_frames.insert(thread.id, frames.stack_frames);
}
}
}
}
let current_thread = if all_threads_stopped {
Some(stopped.thread_id.unwrap_or_default())
} else {
stopped.thread_id
};
let active_frame = current_thread
.and_then(|thread_id| stack_frames.get(&thread_id))
.and_then(|stack_frames| stack_frames.first());
let mut vars = Vec::new();
if let Some(frame) = active_frame {
if let Ok(scopes) = self.dap_rpc.scopes(frame.id) {
for scope in scopes {
let result =
self.dap_rpc.variables(scope.variables_reference);
vars.push((scope, result.unwrap_or_default()));
}
}
}
self.plugin_rpc.core_rpc.dap_stopped(
self.config.dap_id,
stopped.clone(),
stack_frames,
vars,
);
// if all_threads_stopped {
// if let Ok(response) = self.dap_rpc.threads() {
// for thread in response.threads {
// self.fetch_stack_trace(thread.id);
// }
// self.select_thread_id(
// stopped.thread_id.unwrap_or_default(),
// false,
// );
// }
// } else if let Some(thread_id) = stopped.thread_id {
// self.select_thread_id(thread_id, false);
// }
}
DapEvent::Continued(_) => {
self.plugin_rpc.core_rpc.dap_continued(self.dap_rpc.dap_id);
}
DapEvent::Exited(_exited) => {}
DapEvent::Terminated(_) => {
self.terminated = true;
// self.plugin_rpc.core_rpc.dap_terminated(self.dap_rpc.dap_id);
if let Some(term_id) = self.term_id {
self.plugin_rpc.proxy_rpc.terminal_close(term_id);
}
if let Err(err) = self.check_restart() {
tracing::error!("{:?}", err);
}
}
DapEvent::Thread { .. } => {}
DapEvent::Output(_) => {}
DapEvent::Breakpoint { .. } => {}
DapEvent::Module { .. } => {}
DapEvent::LoadedSource { .. } => {}
DapEvent::Process(_) => {}
DapEvent::Capabilities(_) => {}
DapEvent::Memory(_) => {}
}
Ok(())
}
pub(crate) fn initialize(&mut self) -> Result<()> {
let params = dap_types::InitializeParams {
client_id: Some("lapce".to_owned()),
client_name: Some("Lapce".to_owned()),
adapter_id: "".to_string(),
locale: Some("en-us".to_owned()),
lines_start_at_one: Some(true),
columns_start_at_one: Some(true),
path_format: Some("path".to_owned()),
supports_variable_type: Some(true),
supports_variable_paging: Some(false),
supports_run_in_terminal_request: Some(true),
supports_memory_references: Some(false),
supports_progress_reporting: Some(false),
supports_invalidated_event: Some(false),
};
let resp = self
.dap_rpc
.request::<Initialize>(params)
.map_err(|e| anyhow!(e.message))?;
self.capabilities = Some(resp);
Ok(())
}
fn stop(&self) {
let dap_rpc = self.dap_rpc.clone();
if self
.capabilities
.as_ref()
.and_then(|c| c.supports_terminate_request)
.unwrap_or(false)
{
thread::spawn(move || {
if let Err(err) = dap_rpc.terminate() {
tracing::error!("{:?}", err);
}
});
} else {
thread::spawn(move || {
if let Err(err) = dap_rpc.disconnect() {
tracing::error!("{:?}", err);
}
});
}
}
// check if the DAP was restared when we received terminated or disconnected
// if the DAP doesn't supports terminate request, then we also need to wait for
// disconnected
fn check_restart(&mut self) -> Result<()> {
if !self.restarted {
return Ok(());
}
if !self
.capabilities
.as_ref()
.and_then(|c| c.supports_terminate_request)
.unwrap_or(false)
&& !self.disconnected
{
return Ok(());
}
self.restarted = false;
if self.disconnected {
self.start_process()?;
self.initialize()?;
}
self.terminated = false;
self.disconnected = false;
let dap_rpc = self.dap_rpc.clone();
let config = self.config.clone();
thread::spawn(move || {
if let Err(err) = dap_rpc.launch(&config) {
tracing::error!("{:?}", err);
}
});
Ok(())
}
fn restart(&mut self, breakpoints: HashMap<PathBuf, Vec<SourceBreakpoint>>) {
self.restarted = true;
self.breakpoints = breakpoints;
if !self.terminated {
self.stop();
} else if let Err(err) = self.check_restart() {
tracing::error!("{:?}", err);
}
}
}
#[allow(clippy::large_enum_variant)]
pub enum DapRpc {
HostRequest(DapRequest),
HostEvent(DapEvent),
Stop,
Restart(HashMap<PathBuf, Vec<SourceBreakpoint>>),
Shutdown,
Disconnected,
}
#[derive(Clone)]
pub struct DebuggerData {
pub debugger_type: String,
pub program: String,
pub args: Option<Vec<String>>,
}
#[derive(Clone)]
pub struct DapRpcHandler {
pub dap_id: DapId,
rpc_tx: Sender<DapRpc>,
rpc_rx: Receiver<DapRpc>,
io_tx: Sender<DapPayload>,
io_rx: Receiver<DapPayload>,
pub(crate) termain_process_tx: Sender<(TermId, Option<u32>)>,
termain_process_rx: Receiver<(TermId, Option<u32>)>,
seq_counter: Arc<AtomicU64>,
server_pending: Arc<Mutex<HashMap<u64, ResponseHandler<DapResponse, RpcError>>>>,
}
impl DapRpcHandler {
fn new(dap_id: DapId) -> Self {
let (rpc_tx, rpc_rx) = crossbeam_channel::unbounded();
let (io_tx, io_rx) = crossbeam_channel::unbounded();
let (termain_process_tx, termain_process_rx) =
crossbeam_channel::unbounded();
Self {
dap_id,
io_tx,
io_rx,
rpc_rx,
rpc_tx,
termain_process_tx,
termain_process_rx,
seq_counter: Arc::new(AtomicU64::new(0)),
server_pending: Arc::new(Mutex::new(HashMap::new())),
}
}
pub fn mainloop(&self, dap_client: &mut DapClient) {
for msg in &self.rpc_rx {
match msg {
DapRpc::HostRequest(req) => {
let result = dap_client.handle_host_request(&req);
let seq = self.seq_counter.fetch_add(1, Ordering::Relaxed);
let resp = DapResponse {
seq,
request_seq: req.seq,
success: result.is_ok(),
command: req.command.clone(),
message: result.as_ref().err().map(|e| e.to_string()),
body: result.ok(),
};
if let Err(err) = self.io_tx.send(DapPayload::Response(resp)) {
tracing::error!("{:?}", err);
}
}
DapRpc::HostEvent(event) => {
if let Err(err) = dap_client.handle_host_event(&event) {
tracing::error!("{:?}", err);
}
}
DapRpc::Stop => {
dap_client.stop();
}
DapRpc::Restart(breakpoints) => {
dap_client.restart(breakpoints);
}
DapRpc::Shutdown => {
if let Some(term_id) = dap_client.term_id {
dap_client.plugin_rpc.proxy_rpc.terminal_close(term_id);
}
return;
}
DapRpc::Disconnected => {
dap_client.disconnected = true;
if let Some(term_id) = dap_client.term_id {
dap_client.plugin_rpc.proxy_rpc.terminal_close(term_id);
}
if let Err(err) = dap_client.check_restart() {
tracing::error!("{:?}", err);
}
}
}
}
}
fn request_async<R: Request>(
&self,
params: R::Arguments,
f: impl RpcCallback<R::Result, RpcError> + 'static,
) {
self.request_common::<R>(
R::COMMAND,
params,
ResponseHandler::Callback(Box::new(
|result: Result<DapResponse, RpcError>| {
let result = match result {
Ok(resp) => {
if resp.success {
serde_json::from_value(resp.body.into()).map_err(
|e| RpcError {
code: 0,
message: e.to_string(),
},
)
} else {
Err(RpcError {
code: 0,
message: resp.message.unwrap_or_default(),
})
}
}
Err(e) => Err(e),
};
Box::new(f).call(result);
},
)),
);
}
fn request<R: Request>(
&self,
params: R::Arguments,
) -> Result<R::Result, RpcError> {
let (tx, rx) = crossbeam_channel::bounded(1);
self.request_common::<R>(R::COMMAND, params, ResponseHandler::Chan(tx));
let resp = rx
.recv_timeout(std::time::Duration::from_secs(30))
.map_err(|_| RpcError {
code: 0,
message: "io error".to_string(),
})??;
if resp.success {
let resp: R::Result =
serde_json::from_value(resp.body.into()).map_err(|e| RpcError {
code: 0,
message: e.to_string(),
})?;
Ok(resp)
} else {
Err(RpcError {
code: 0,
message: resp.message.unwrap_or_default(),
})
}
}
fn request_common<R: Request>(
&self,
command: &'static str,
arguments: R::Arguments,
rh: ResponseHandler<DapResponse, RpcError>,
) {
let seq = self.seq_counter.fetch_add(1, Ordering::Relaxed);
let arguments: Value = serde_json::to_value(arguments).unwrap();
{
let mut pending = self.server_pending.lock();
pending.insert(seq, rh);
}
if let Err(err) = self.io_tx.send(DapPayload::Request(DapRequest {
seq,
command: command.to_string(),
arguments: Some(arguments),
})) {
tracing::error!("{:?}", err);
}
}
fn handle_server_response(&self, resp: DapResponse) {
if let Some(rh) = { self.server_pending.lock().remove(&resp.request_seq) } {
rh.invoke(Ok(resp));
}
}
pub fn handle_server_message(&self, message_str: &str) {
if let Ok(payload) = serde_json::from_str::<DapPayload>(message_str) {
match payload {
DapPayload::Request(req) => {
if let Err(err) = self.rpc_tx.send(DapRpc::HostRequest(req)) {
tracing::error!("{:?}", err);
}
}
DapPayload::Event(event) => {
if let Err(err) = self.rpc_tx.send(DapRpc::HostEvent(event)) {
tracing::error!("{:?}", err);
}
}
DapPayload::Response(resp) => {
self.handle_server_response(resp);
}
}
}
}
pub fn launch(&self, config: &RunDebugConfig) -> Result<()> {
let params = serde_json::json!({
"program": config.program,
"args": config.args,
"cwd": config.cwd,
"runInTerminal": true,
"env": config.env
});
let _resp = self
.request::<Launch>(params)
.map_err(|e| anyhow!(e.message))?;
Ok(())
}
pub fn stop(&self) {
if let Err(err) = self.rpc_tx.send(DapRpc::Stop) {
tracing::error!("{:?}", err);
}
}
pub fn restart(&self, breakpoints: HashMap<PathBuf, Vec<SourceBreakpoint>>) {
if let Err(err) = self.rpc_tx.send(DapRpc::Restart(breakpoints)) {
tracing::error!("{:?}", err);
}
}
fn disconnected(&self) {
if let Err(err) = self.rpc_tx.send(DapRpc::Disconnected) {
tracing::error!("{:?}", err);
}
}
pub fn disconnect(&self) -> Result<()> {
self.request::<Disconnect>(())
.map_err(|e| anyhow!(e.message))?;
Ok(())
}
fn terminate(&self) -> Result<()> {
self.request::<Terminate>(())
.map_err(|e| anyhow!(e.message))?;
Ok(())
}
pub fn set_breakpoints_async(
&self,
file: PathBuf,
breakpoints: Vec<SourceBreakpoint>,
f: impl RpcCallback<SetBreakpointsResponse, RpcError> + 'static,
) {
let params = SetBreakpointsArguments {
source: Source {
path: Some(file),
name: None,
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
},
breakpoints: Some(breakpoints),
source_modified: Some(false),
};
self.request_async::<SetBreakpoints>(params, f);
}
pub fn set_breakpoints(
&self,
file: PathBuf,
breakpoints: Vec<SourceBreakpoint>,
) -> Result<SetBreakpointsResponse> {
let params = SetBreakpointsArguments {
source: Source {
path: Some(file),
name: None,
source_reference: None,
presentation_hint: None,
origin: None,
sources: None,
adapter_data: None,
checksums: None,
},
breakpoints: Some(breakpoints),
source_modified: Some(false),
};
let resp = self
.request::<SetBreakpoints>(params)
.map_err(|e| anyhow!(e.message))?;
Ok(resp)
}
pub fn continue_thread(&self, thread_id: ThreadId) -> Result<ContinueResponse> {
let params = ContinueArguments { thread_id };
let resp = self
.request::<Continue>(params)
.map_err(|e| anyhow!(e.message))?;
Ok(resp)
}
pub fn pause_thread(&self, thread_id: ThreadId) -> Result<()> {
let params = PauseArguments { thread_id };
self.request::<Pause>(params)
.map_err(|e| anyhow!(e.message))?;
Ok(())
}
pub fn threads(&self) -> Result<ThreadsResponse> {
let resp = self
.request::<Threads>(())
.map_err(|e| anyhow!(e.message))?;
Ok(resp)
}
pub fn stack_trace(&self, thread_id: ThreadId) -> Result<StackTraceResponse> {
let params = StackTraceArguments {
thread_id,
..Default::default()
};
let resp = self
.request::<StackTrace>(params)
.map_err(|e| anyhow!(e.message))?;
Ok(resp)
}
pub fn scopes(&self, frame_id: usize) -> Result<Vec<Scope>> {
let args = ScopesArguments { frame_id };
let response = self
.request::<Scopes>(args)
.map_err(|e| anyhow!(e.message))?;
Ok(response.scopes)
}
pub fn scopes_async(
&self,
frame_id: usize,
f: impl RpcCallback<ScopesResponse, RpcError> + 'static,
) {
let args = ScopesArguments { frame_id };
self.request_async::<Scopes>(args, f);
}
pub fn variables(&self, variables_reference: usize) -> Result<Vec<Variable>> {
let args = VariablesArguments {
variables_reference,
filter: None,
start: None,
count: None,
format: None,
};
let response = self
.request::<Variables>(args)
.map_err(|e| anyhow!(e.message))?;
Ok(response.variables)
}
pub fn variables_async(
&self,
variables_reference: usize,
f: impl RpcCallback<VariablesResponse, RpcError> + 'static,
) {
let args = VariablesArguments {
variables_reference,
filter: None,
start: None,
count: None,
format: None,
};
self.request_async::<Variables>(args, f);
}
pub fn next(&self, thread_id: ThreadId) {
let args = NextArguments {
thread_id,
granularity: None,
};
self.request_async::<Next>(args, move |_| {});
}
pub fn step_in(&self, thread_id: ThreadId) {
let args = StepInArguments {
thread_id,
target_id: None,
granularity: None,
};
self.request_async::<StepIn>(args, move |_| {});
}
pub fn step_out(&self, thread_id: ThreadId) {
let args = StepOutArguments {
thread_id,
granularity: None,
};
self.request_async::<StepOut>(args, move |_| {});
}
}
+557
View File
@@ -0,0 +1,557 @@
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use std::{
io::{BufRead, BufReader, BufWriter, Write},
path::{Path, PathBuf},
process::{self, Child, Command, Stdio},
sync::Arc,
thread,
};
use anyhow::{Result, anyhow};
use jsonrpc_lite::{Id, Params};
use lapce_core::meta;
use lapce_rpc::{
RpcError,
plugin::{PluginId, VoltID},
style::LineStyle,
};
use lapce_xi_rope::Rope;
use lsp_types::{
notification::{Initialized, Notification},
request::{Initialize, Request},
*,
};
use parking_lot::Mutex;
use serde_json::Value;
use super::{
client_capabilities,
psp::{
PluginHandlerNotification, PluginHostHandler, PluginServerHandler,
PluginServerRpcHandler, ResponseSender, RpcCallback,
handle_plugin_server_message,
},
};
use crate::{buffer::Buffer, plugin::PluginCatalogRpcHandler};
const HEADER_CONTENT_LENGTH: &str = "content-length";
const HEADER_CONTENT_TYPE: &str = "content-type";
pub enum LspRpc {
Request {
id: u64,
method: String,
params: Params,
},
Notification {
method: String,
params: Params,
},
Response {
id: u64,
result: Value,
},
Error {
id: u64,
error: RpcError,
},
}
pub struct LspClient {
plugin_rpc: PluginCatalogRpcHandler,
server_rpc: PluginServerRpcHandler,
process: Child,
workspace: Option<PathBuf>,
host: PluginHostHandler,
options: Option<Value>,
}
impl PluginServerHandler for LspClient {
fn method_registered(&mut self, method: &str) -> bool {
self.host.method_registered(method)
}
fn document_supported(
&mut self,
language_id: Option<&str>,
path: Option<&Path>,
) -> bool {
self.host.document_supported(language_id, path)
}
fn handle_handler_notification(
&mut self,
notification: PluginHandlerNotification,
) {
use PluginHandlerNotification::*;
match notification {
Initialize => {
self.initialize();
}
InitializeResult(result) => {
self.host.server_capabilities = result.capabilities;
}
Shutdown => {
self.shutdown();
}
SpawnedPluginLoaded { .. } => {}
}
}
fn handle_host_request(
&mut self,
id: Id,
method: String,
params: Params,
resp: ResponseSender,
) {
self.host.handle_request(id, method, params, resp);
}
fn handle_host_notification(
&mut self,
method: String,
params: Params,
from: String,
) {
if let Err(err) = self.host.handle_notification(method, params, from) {
tracing::error!("{:?}", err);
}
}
fn handle_did_save_text_document(
&self,
language_id: String,
path: PathBuf,
text_document: TextDocumentIdentifier,
text: lapce_xi_rope::Rope,
) {
self.host.handle_did_save_text_document(
language_id,
path,
text_document,
text,
);
}
fn handle_did_change_text_document(
&mut self,
language_id: String,
document: lsp_types::VersionedTextDocumentIdentifier,
delta: lapce_xi_rope::RopeDelta,
text: lapce_xi_rope::Rope,
new_text: lapce_xi_rope::Rope,
change: Arc<
Mutex<(
Option<TextDocumentContentChangeEvent>,
Option<TextDocumentContentChangeEvent>,
)>,
>,
) {
self.host.handle_did_change_text_document(
language_id,
document,
delta,
text,
new_text,
change,
);
}
fn format_semantic_tokens(
&self,
tokens: SemanticTokens,
text: Rope,
f: Box<dyn RpcCallback<Vec<LineStyle>, RpcError>>,
) {
self.host.format_semantic_tokens(tokens, text, f);
}
}
impl LspClient {
#[allow(clippy::too_many_arguments)]
fn new(
plugin_rpc: PluginCatalogRpcHandler,
document_selector: DocumentSelector,
workspace: Option<PathBuf>,
volt_id: VoltID,
volt_display_name: String,
spawned_by: Option<PluginId>,
plugin_id: Option<PluginId>,
pwd: Option<PathBuf>,
server_uri: Url,
args: Vec<String>,
options: Option<Value>,
) -> Result<Self> {
let server = match server_uri.scheme() {
"file" => {
let path = server_uri.to_file_path().map_err(|_| anyhow!(""))?;
#[cfg(unix)]
if let Err(err) = std::process::Command::new("chmod")
.arg("+x")
.arg(&path)
.output()
{
tracing::error!("{:?}", err);
}
path.to_str().ok_or_else(|| anyhow!(""))?.to_string()
}
"urn" => server_uri.path().to_string(),
_ => return Err(anyhow!("uri not supported")),
};
let mut process = Self::process(workspace.as_ref(), &server, &args)?;
let stdin = process.stdin.take().unwrap();
let stdout = process.stdout.take().unwrap();
let stderr = process.stderr.take().unwrap();
let mut writer = Box::new(BufWriter::new(stdin));
let (io_tx, io_rx) = crossbeam_channel::unbounded();
let server_rpc = PluginServerRpcHandler::new(
volt_id.clone(),
spawned_by,
plugin_id,
io_tx.clone(),
);
thread::spawn(move || {
for msg in io_rx {
if msg
.get_method()
.map(|x| x == lsp_types::request::Shutdown::METHOD)
.unwrap_or_default()
{
break;
}
if let Ok(msg) = serde_json::to_string(&msg) {
tracing::debug!("write to lsp: {}", msg);
let msg =
format!("Content-Length: {}\r\n\r\n{}", msg.len(), msg);
if let Err(err) = writer.write(msg.as_bytes()) {
tracing::error!("{:?}", err);
}
if let Err(err) = writer.flush() {
tracing::error!("{:?}", err);
}
}
}
});
let local_server_rpc = server_rpc.clone();
let core_rpc = plugin_rpc.core_rpc.clone();
let volt_id_closure = volt_id.clone();
let name = volt_display_name.clone();
thread::spawn(move || {
let mut reader = Box::new(BufReader::new(stdout));
loop {
match read_message(&mut reader) {
Ok(message_str) => {
if !message_str.contains("$/progress") {
tracing::debug!("read from lsp: {}", message_str);
}
if let Some(resp) = handle_plugin_server_message(
&local_server_rpc,
&message_str,
&name,
) {
if let Err(err) = io_tx.send(resp) {
tracing::error!("{:?}", err);
}
}
}
Err(_err) => {
core_rpc.log(
lapce_rpc::core::LogLevel::Error,
format!("lsp server {server} stopped!"),
Some(format!(
"lapce_proxy::plugin::lsp::{}::{}::stopped",
volt_id_closure.author, volt_id_closure.name
)),
);
return;
}
};
}
});
let core_rpc = plugin_rpc.core_rpc.clone();
let volt_id_closure = volt_id.clone();
thread::spawn(move || {
let mut reader = Box::new(BufReader::new(stderr));
loop {
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(n) => {
if n == 0 {
return;
}
core_rpc.log(
lapce_rpc::core::LogLevel::Trace,
line.trim_end().to_string(),
Some(format!(
"lapce_proxy::plugin::lsp::{}::{}::stderr",
volt_id_closure.author, volt_id_closure.name
)),
);
}
Err(_) => {
return;
}
}
}
});
let host = PluginHostHandler::new(
workspace.clone(),
pwd,
volt_id,
volt_display_name,
document_selector,
plugin_rpc.core_rpc.clone(),
server_rpc.clone(),
plugin_rpc.clone(),
);
Ok(Self {
plugin_rpc,
server_rpc,
process,
workspace,
host,
options,
})
}
#[allow(clippy::too_many_arguments)]
pub fn start(
plugin_rpc: PluginCatalogRpcHandler,
document_selector: DocumentSelector,
workspace: Option<PathBuf>,
volt_id: VoltID,
volt_display_name: String,
spawned_by: Option<PluginId>,
plugin_id: Option<PluginId>,
pwd: Option<PathBuf>,
server_uri: Url,
args: Vec<String>,
options: Option<Value>,
) -> Result<PluginId> {
let mut lsp = Self::new(
plugin_rpc,
document_selector,
workspace,
volt_id,
volt_display_name,
spawned_by,
plugin_id,
pwd,
server_uri,
args,
options,
)?;
let plugin_id = lsp.server_rpc.plugin_id;
let rpc = lsp.server_rpc.clone();
thread::spawn(move || {
rpc.mainloop(&mut lsp);
});
Ok(plugin_id)
}
fn initialize(&mut self) {
let root_uri = self
.workspace
.clone()
.map(|p| Url::from_directory_path(p).unwrap());
tracing::debug!("initialization_options {:?}", self.options);
#[allow(deprecated)]
let params = InitializeParams {
process_id: Some(process::id()),
root_uri: root_uri.clone(),
initialization_options: self.options.clone(),
capabilities: client_capabilities(),
trace: Some(TraceValue::Verbose),
workspace_folders: root_uri.map(|uri| {
vec![WorkspaceFolder {
name: uri.as_str().to_string(),
uri,
}]
}),
client_info: Some(ClientInfo {
name: meta::NAME.to_owned(),
version: Some(meta::VERSION.to_owned()),
}),
locale: None,
root_path: None,
work_done_progress_params: WorkDoneProgressParams::default(),
};
match self.server_rpc.server_request(
Initialize::METHOD,
params,
None,
None,
false,
) {
Ok(value) => {
let result: InitializeResult =
serde_json::from_value(value).unwrap();
self.host.server_capabilities = result.capabilities;
self.server_rpc.server_notification(
Initialized::METHOD,
InitializedParams {},
None,
None,
false,
);
if self
.plugin_rpc
.plugin_server_loaded(self.server_rpc.clone())
.is_err()
{
self.server_rpc.shutdown();
self.shutdown();
}
}
Err(err) => {
tracing::error!("{:?}", err);
}
}
// move |result| {
// if let Ok(value) = result {
// let result: InitializeResult =
// serde_json::from_value(value).unwrap();
// server_rpc.handle_rpc(PluginServerRpc::Handler(
// PluginHandlerNotification::InitializeDone(result),
// ));
// }
// },
// );
}
fn shutdown(&mut self) {
if let Err(err) = self.process.kill() {
tracing::error!("{:?}", err);
}
if let Err(err) = self.process.wait() {
tracing::error!("{:?}", err);
}
}
fn process(
workspace: Option<&PathBuf>,
server: &str,
args: &[String],
) -> Result<Child> {
let mut process = Command::new(server);
if let Some(workspace) = workspace {
process.current_dir(workspace);
}
process.args(args);
#[cfg(target_os = "windows")]
let process = process.creation_flags(0x08000000);
let child = process
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
Ok(child)
}
}
pub struct DocumentFilter {
/// The document must have this language id, if it exists
pub language_id: Option<String>,
/// The document's path must match this glob, if it exists
pub pattern: Option<globset::GlobMatcher>,
// TODO: URI Scheme from lsp-types document filter
}
impl DocumentFilter {
/// Constructs a document filter from the LSP version
/// This ignores any fields that are badly constructed
pub(crate) fn from_lsp_filter_loose(
filter: &lsp_types::DocumentFilter,
) -> DocumentFilter {
DocumentFilter {
language_id: filter.language.clone(),
// TODO: clean this up
pattern: filter
.pattern
.as_deref()
.map(globset::Glob::new)
.and_then(Result::ok)
.map(|x| globset::Glob::compile_matcher(&x)),
}
}
}
pub enum LspHeader {
ContentType,
ContentLength(usize),
}
fn parse_header(s: &str) -> Result<LspHeader> {
let split: Vec<String> =
s.splitn(2, ": ").map(|s| s.trim().to_lowercase()).collect();
if split.len() != 2 {
return Err(anyhow!("Malformed"));
};
match split[0].as_ref() {
HEADER_CONTENT_TYPE => Ok(LspHeader::ContentType),
HEADER_CONTENT_LENGTH => {
Ok(LspHeader::ContentLength(split[1].parse::<usize>()?))
}
_ => Err(anyhow!("Unknown parse error occurred")),
}
}
pub fn read_message<T: BufRead>(reader: &mut T) -> Result<String> {
let mut buffer = String::new();
let mut content_length: Option<usize> = None;
loop {
buffer.clear();
let _ = reader.read_line(&mut buffer)?;
// eprin
match &buffer {
s if s.trim().is_empty() => break,
s => {
match parse_header(s)? {
LspHeader::ContentLength(len) => content_length = Some(len),
LspHeader::ContentType => (),
};
}
};
}
let content_length = content_length
.ok_or_else(|| anyhow!("missing content-length header: {}", buffer))?;
let mut body_buffer = vec![0; content_length];
reader.read_exact(&mut body_buffer)?;
let body = String::from_utf8(body_buffer)?;
Ok(body)
}
pub fn get_change_for_sync_kind(
sync_kind: TextDocumentSyncKind,
buffer: &Buffer,
content_change: &TextDocumentContentChangeEvent,
) -> Option<Vec<TextDocumentContentChangeEvent>> {
match sync_kind {
TextDocumentSyncKind::NONE => None,
TextDocumentSyncKind::FULL => {
let text_document_content_change_event =
TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: buffer.get_document(),
};
Some(vec![text_document_content_change_event])
}
TextDocumentSyncKind::INCREMENTAL => Some(vec![content_change.clone()]),
_ => None,
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+629
View File
@@ -0,0 +1,629 @@
#[cfg(test)]
mod tests;
use std::{
collections::{HashMap, VecDeque},
fs,
io::{Read, Seek, Write},
path::{Path, PathBuf},
process,
sync::{Arc, RwLock},
thread,
};
use anyhow::{Result, anyhow};
use jsonrpc_lite::{Id, Params};
use lapce_core::directory::Directory;
use lapce_rpc::{
RpcError,
plugin::{PluginId, VoltID, VoltInfo, VoltMetadata},
style::LineStyle,
};
use lapce_xi_rope::{Rope, RopeDelta};
use lsp_types::{
DocumentFilter, InitializeParams, InitializedParams,
TextDocumentContentChangeEvent, TextDocumentIdentifier, Url,
VersionedTextDocumentIdentifier, WorkDoneProgressParams, WorkspaceFolder,
notification::Initialized, request::Initialize,
};
use parking_lot::Mutex;
use psp_types::{Notification, Request};
use serde_json::Value;
use wasi_experimental_http_wasmtime::{HttpCtx, HttpState};
use wasmtime_wasi::WasiCtxBuilder;
use super::{
PluginCatalogRpcHandler, client_capabilities,
psp::{
PluginHandlerNotification, PluginHostHandler, PluginServerHandler,
PluginServerRpc, ResponseSender, RpcCallback, handle_plugin_server_message,
},
volt_icon,
};
use crate::plugin::psp::PluginServerRpcHandler;
#[derive(Default)]
pub struct WasiPipe {
buffer: VecDeque<u8>,
}
impl WasiPipe {
pub fn new() -> Self {
Self::default()
}
}
impl Read for WasiPipe {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let amt = std::cmp::min(buf.len(), self.buffer.len());
for (i, byte) in self.buffer.drain(..amt).enumerate() {
buf[i] = byte;
}
Ok(amt)
}
}
impl Write for WasiPipe {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.buffer.extend(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Seek for WasiPipe {
fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
Err(std::io::Error::other("can not seek in a pipe"))
}
}
pub struct Plugin {
#[allow(dead_code)]
id: PluginId,
host: PluginHostHandler,
configurations: Option<HashMap<String, serde_json::Value>>,
}
impl PluginServerHandler for Plugin {
fn method_registered(&mut self, method: &str) -> bool {
self.host.method_registered(method)
}
fn document_supported(
&mut self,
language_id: Option<&str>,
path: Option<&Path>,
) -> bool {
self.host.document_supported(language_id, path)
}
fn handle_handler_notification(
&mut self,
notification: PluginHandlerNotification,
) {
use PluginHandlerNotification::*;
match notification {
Initialize => {
self.initialize();
}
InitializeResult(result) => {
self.host.server_capabilities = result.capabilities;
}
Shutdown => {
self.shutdown();
}
SpawnedPluginLoaded { plugin_id } => {
self.host.handle_spawned_plugin_loaded(plugin_id);
}
}
}
fn handle_host_notification(
&mut self,
method: String,
params: Params,
from: String,
) {
if let Err(err) = self.host.handle_notification(method, params, from) {
tracing::error!("{:?}", err);
}
}
fn handle_host_request(
&mut self,
id: Id,
method: String,
params: Params,
resp: ResponseSender,
) {
self.host.handle_request(id, method, params, resp);
}
fn handle_did_save_text_document(
&self,
language_id: String,
path: PathBuf,
text_document: TextDocumentIdentifier,
text: Rope,
) {
self.host.handle_did_save_text_document(
language_id,
path,
text_document,
text,
);
}
fn handle_did_change_text_document(
&mut self,
language_id: String,
document: VersionedTextDocumentIdentifier,
delta: RopeDelta,
text: Rope,
new_text: Rope,
change: Arc<
Mutex<(
Option<TextDocumentContentChangeEvent>,
Option<TextDocumentContentChangeEvent>,
)>,
>,
) {
self.host.handle_did_change_text_document(
language_id,
document,
delta,
text,
new_text,
change,
);
}
fn format_semantic_tokens(
&self,
tokens: lsp_types::SemanticTokens,
text: Rope,
f: Box<dyn RpcCallback<Vec<LineStyle>, RpcError>>,
) {
self.host.format_semantic_tokens(tokens, text, f);
}
}
impl Plugin {
fn initialize(&mut self) {
let workspace = self.host.workspace.clone();
let configurations = self.configurations.as_ref().map(unflatten_map);
let root_uri = workspace.map(|p| Url::from_directory_path(p).unwrap());
let server_rpc = self.host.server_rpc.clone();
self.host.server_rpc.server_request_async(
Initialize::METHOD,
#[allow(deprecated)]
InitializeParams {
process_id: Some(process::id()),
root_path: None,
root_uri: root_uri.clone(),
capabilities: client_capabilities(),
trace: None,
client_info: None,
locale: None,
initialization_options: configurations,
workspace_folders: root_uri.map(|uri| {
vec![WorkspaceFolder {
name: uri.as_str().to_string(),
uri,
}]
}),
work_done_progress_params: WorkDoneProgressParams::default(),
},
None,
None,
false,
move |value| match value {
Ok(value) => {
if let Ok(result) = serde_json::from_value(value) {
server_rpc.handle_rpc(PluginServerRpc::Handler(
PluginHandlerNotification::InitializeResult(result),
));
server_rpc.server_notification(
Initialized::METHOD,
InitializedParams {},
None,
None,
false,
);
}
}
Err(err) => {
tracing::error!("{:?}", err);
}
},
);
}
fn shutdown(&self) {}
}
pub fn load_all_volts(
plugin_rpc: PluginCatalogRpcHandler,
extra_plugin_paths: &[PathBuf],
disabled_volts: Vec<VoltID>,
) {
let all_volts = find_all_volts(extra_plugin_paths);
let volts = all_volts
.into_iter()
.filter_map(|meta| {
meta.wasm.as_ref()?;
let icon = volt_icon(&meta);
plugin_rpc.core_rpc.volt_installed(meta.clone(), icon);
if disabled_volts.contains(&meta.id()) {
return None;
}
Some(meta)
})
.collect();
if let Err(err) = plugin_rpc.unactivated_volts(volts) {
tracing::error!("{:?}", err);
}
}
/// Find all installed volts.
/// `plugin_dev_path` allows launching Lapce with a plugin on your local system for testing
/// purposes.
/// As well, this function skips any volt in the typical plugin directory that match the name
/// of the dev plugin so as to support developing a plugin you actively use.
pub fn find_all_volts(extra_plugin_paths: &[PathBuf]) -> Vec<VoltMetadata> {
let Some(plugin_dir) = Directory::plugins_directory() else {
return Vec::new();
};
let mut plugins: Vec<VoltMetadata> = plugin_dir
.read_dir()
.ok()
.into_iter()
.flatten()
.filter_map(|result| {
let entry = result.ok()?;
let metadata = entry.metadata().ok()?;
// Ignore any loose files or '.' prefixed hidden directories
if metadata.is_file() || entry.file_name().to_str()?.starts_with('.') {
return None;
}
Some(entry.path())
})
.filter_map(|path| match load_volt(&path) {
Ok(metadata) => Some(metadata),
Err(e) => {
tracing::error!("Failed to load plugin: {:?}", e);
None
}
})
.collect();
for plugin_path in extra_plugin_paths {
let mut metadata = match load_volt(plugin_path) {
Ok(metadata) => metadata,
Err(e) => {
tracing::error!("Failed to load extra plugin: {:?}", e);
continue;
}
};
let pos = plugins.iter().position(|meta| {
meta.name == metadata.name && meta.author == metadata.author
});
if let Some(pos) = pos {
std::mem::swap(&mut plugins[pos], &mut metadata);
} else {
plugins.push(metadata);
}
}
plugins
}
/// Returns an instance of "VoltMetadata" or an error if there is no file in the path,
/// the contents of the file cannot be read into a string, or the content read cannot
/// be converted to an instance of "VoltMetadata".
///
/// # Examples
///
/// ```
/// use std::fs::File;
/// use std::io::Write;
/// use lapce_proxy::plugin::wasi::load_volt;
/// use lapce_rpc::plugin::VoltMetadata;
///
/// let parent_path = std::env::current_dir().unwrap();
/// let mut file = File::create(parent_path.join("volt.toml")).unwrap();
/// let _ = writeln!(file, "name = \"plugin\" \n version = \"0.1\"");
/// let _ = writeln!(file, "display-name = \"Plugin\" \n author = \"Author\"");
/// let _ = writeln!(file, "description = \"Useful plugin\"");///
/// let volt_metadata = match load_volt(&parent_path) {
/// Ok(volt_metadata) => volt_metadata,
/// Err(error) => panic!("{}", error),
/// };
/// assert_eq!(
/// volt_metadata,
/// VoltMetadata {
/// name: "plugin".to_string(),
/// version: "0.1".to_string(),
/// display_name: "Plugin".to_string(),
/// author: "Author".to_string(),
/// description: "Useful plugin".to_string(),
/// icon: None,
/// repository: None,
/// wasm: None,
/// color_themes: None,
/// icon_themes: None,
/// dir: parent_path.canonicalize().ok(),
/// activation: None,
/// config: None
/// }
/// );
/// let _ = std::fs::remove_file(parent_path.join("volt.toml"));
/// ```
pub fn load_volt(path: &Path) -> Result<VoltMetadata> {
let path = path.canonicalize()?;
let mut file = fs::File::open(path.join("volt.toml"))?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let mut meta: VoltMetadata = toml::from_str(&contents)?;
meta.dir = Some(path.clone());
meta.wasm = meta.wasm.as_ref().and_then(|wasm| {
Some(path.join(wasm).canonicalize().ok()?.to_str()?.to_string())
});
// FIXME: This does `meta.color_themes = Some([])` in case, for example,
// it cannot find matching files, but in that case it should do `meta.color_themes = None`
meta.color_themes = meta.color_themes.as_ref().map(|themes| {
themes
.iter()
.filter_map(|theme| {
Some(path.join(theme).canonicalize().ok()?.to_str()?.to_string())
})
.collect()
});
// FIXME: This does `meta.icon_themes = Some([])` in case, for example,
// it cannot find matching files, but in that case it should do `meta.icon_themes = None`
meta.icon_themes = meta.icon_themes.as_ref().map(|themes| {
themes
.iter()
.filter_map(|theme| {
Some(path.join(theme).canonicalize().ok()?.to_str()?.to_string())
})
.collect()
});
Ok(meta)
}
pub fn enable_volt(
plugin_rpc: PluginCatalogRpcHandler,
volt: VoltInfo,
) -> Result<()> {
let path = Directory::plugins_directory()
.ok_or_else(|| anyhow!("can't get plugin directory"))?
.join(volt.id().to_string());
let meta = load_volt(&path)?;
plugin_rpc.unactivated_volts(vec![meta])?;
Ok(())
}
pub fn start_volt(
workspace: Option<PathBuf>,
configurations: Option<HashMap<String, serde_json::Value>>,
plugin_rpc: PluginCatalogRpcHandler,
meta: VoltMetadata,
) -> Result<()> {
let engine = wasmtime::Engine::default();
let module = wasmtime::Module::from_file(
&engine,
meta.wasm
.as_ref()
.ok_or_else(|| anyhow!("no wasm in plugin"))?,
)?;
let mut linker = wasmtime::Linker::new(&engine);
wasmtime_wasi::add_to_linker(&mut linker, |s| s)?;
HttpState::new()?.add_to_linker(&mut linker, |_| HttpCtx {
allowed_hosts: Some(vec!["insecure:allow-all".to_string()]),
max_concurrent_requests: Some(100),
})?;
let volt_path = meta
.dir
.as_ref()
.ok_or_else(|| anyhow!("plugin meta doesn't have dir"))?;
#[cfg(target_os = "linux")]
let volt_libc = {
match std::process::Command::new("ldd").arg("--version").output() {
Ok(cmd) => {
if String::from_utf8_lossy(&cmd.stdout)
.to_lowercase()
.split_terminator('\n')
.next()
.unwrap_or("")
.contains("musl")
{
"musl"
} else {
"glibc"
}
}
_ => "glibc",
}
};
#[cfg(not(target_os = "linux"))]
let volt_libc = "";
let stdin = Arc::new(RwLock::new(WasiPipe::new()));
let stdout = Arc::new(RwLock::new(WasiPipe::new()));
let stderr = Arc::new(RwLock::new(WasiPipe::new()));
let wasi = WasiCtxBuilder::new()
.inherit_env()?
.env("VOLT_OS", std::env::consts::OS)?
.env("VOLT_ARCH", std::env::consts::ARCH)?
.env("VOLT_LIBC", volt_libc)?
.env(
"VOLT_URI",
Url::from_directory_path(volt_path)
.map_err(|_| anyhow!("can't convert folder path to uri"))?
.as_ref(),
)?
.stdin(Box::new(wasi_common::pipe::ReadPipe::from_shared(
stdin.clone(),
)))
.stdout(Box::new(wasi_common::pipe::WritePipe::from_shared(
stdout.clone(),
)))
.stderr(Box::new(wasi_common::pipe::WritePipe::from_shared(
stderr.clone(),
)))
.preopened_dir(
wasmtime_wasi::Dir::open_ambient_dir(
volt_path,
wasmtime_wasi::ambient_authority(),
)?,
"/",
)?
.build();
let mut store = wasmtime::Store::new(&engine, wasi);
let (io_tx, io_rx) = crossbeam_channel::unbounded();
let rpc = PluginServerRpcHandler::new(meta.id(), None, None, io_tx);
let local_rpc = rpc.clone();
let local_stdin = stdin.clone();
let volt_name = format!("volt {}", meta.name);
linker.func_wrap("lapce", "host_handle_rpc", move || {
if let Ok(msg) = wasi_read_string(&stdout) {
if let Some(resp) =
handle_plugin_server_message(&local_rpc, &msg, &volt_name)
{
if let Ok(msg) = serde_json::to_string(&resp) {
if let Err(err) = writeln!(local_stdin.write().unwrap(), "{msg}")
{
tracing::error!("{:?}", err);
}
}
}
}
})?;
let plugin_meta = meta.clone();
linker.func_wrap("lapce", "host_handle_stderr", move || {
if let Ok(msg) = wasi_read_string(&stderr) {
tracing_log::log::log!(target: &format!("lapce_proxy::plugin::wasi::{}::{}", plugin_meta.author, plugin_meta.name), tracing_log::log::Level::Debug, "{msg}");
}
})?;
linker.module(&mut store, "", &module)?;
let local_rpc = rpc.clone();
thread::spawn(move || {
let mut exist_id = None;
{
let instance = linker.instantiate(&mut store, &module).unwrap();
let handle_rpc = instance
.get_func(&mut store, "handle_rpc")
.ok_or_else(|| anyhow!("can't convert to function"))
.unwrap()
.typed::<(), ()>(&mut store)
.unwrap();
for msg in io_rx {
if msg
.get_method()
.map(|x| x == lsp_types::request::Shutdown::METHOD)
.unwrap_or_default()
{
exist_id = msg.get_id();
break;
}
if let Ok(msg) = serde_json::to_string(&msg) {
if let Err(err) = writeln!(stdin.write().unwrap(), "{msg}") {
tracing::error!("{:?}", err);
}
}
if let Err(err) = handle_rpc.call(&mut store, ()) {
tracing::error!("{:?}", err);
}
}
}
if let Some(id) = exist_id {
local_rpc.handle_server_response(id, Ok(Value::Null));
}
});
let id = PluginId::next();
let mut plugin = Plugin {
id,
host: PluginHostHandler::new(
workspace,
meta.dir.clone(),
meta.id(),
meta.display_name.clone(),
meta.activation
.iter()
.flat_map(|m| m.language.iter().flatten())
.cloned()
.map(|s| DocumentFilter {
language: Some(s),
pattern: None,
scheme: None,
})
.chain(
meta.activation
.iter()
.flat_map(|m| m.workspace_contains.iter().flatten())
.cloned()
.map(|s| DocumentFilter {
language: None,
pattern: Some(s),
scheme: None,
}),
)
.collect(),
plugin_rpc.core_rpc.clone(),
rpc.clone(),
plugin_rpc.clone(),
),
configurations,
};
let local_rpc = rpc.clone();
thread::spawn(move || {
local_rpc.mainloop(&mut plugin);
});
if plugin_rpc.plugin_server_loaded(rpc.clone()).is_err() {
rpc.shutdown();
}
Ok(())
}
fn wasi_read_string(stdout: &Arc<RwLock<WasiPipe>>) -> Result<String> {
let mut buf = String::new();
stdout.write().unwrap().read_to_string(&mut buf)?;
Ok(buf)
}
fn unflatten_map(map: &HashMap<String, serde_json::Value>) -> serde_json::Value {
let mut new = serde_json::json!({});
for (key, value) in map.iter() {
let mut current = new.as_object_mut().unwrap();
let parts: Vec<&str> = key.split('.').collect();
let total_parts = parts.len();
for (i, part) in parts.into_iter().enumerate() {
if i + 1 < total_parts {
if !current.get(part).map(|v| v.is_object()).unwrap_or(false) {
current.insert(part.to_string(), serde_json::json!({}));
}
current = current.get_mut(part).unwrap().as_object_mut().unwrap();
} else {
current.insert(part.to_string(), value.clone());
}
}
}
new
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
viewBox="0 0 26.458333 26.458333"
version="1.1"
id="svg169"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="рисунок.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview171"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="5.9651515"
inkscape:cx="41.490983"
inkscape:cy="49.286259"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs166" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
style="fill:#000000;fill-opacity:1;stroke:#0000ff;stroke-width:2.0673;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path483"
cx="13.173385"
cy="13.48387"
rx="12.114121"
ry="11.892347" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
viewBox="0 0 26.458333 26.458333"
version="1.1"
id="svg169"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="рисунок1.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview171"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="5.9651515"
inkscape:cx="41.490983"
inkscape:cy="49.286259"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs166" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:#0000ff;stroke-width:2.0673;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path483"
cx="13.173385"
cy="13.48387"
rx="12.114121"
ry="11.892347" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,16 @@
name = "some-useful-plugin"
version = "0.1.56"
display-name = "Some Useful Plugin Name"
author = "some_author"
description = "very useful plugin"
icon = "icon.svg"
repository = "https://github.com/lapce"
wasm = "lapce.wasm"
color-themes = [
"Dark.toml",
"Light.toml",
]
icon-themes = [
"Dark.svg",
"Light.svg",
]
@@ -0,0 +1,16 @@
name = "some-useful-plugin"
version = "0.1.56"
display-name = "Some Useful Plugin Name"
author = "some_author"
description = "very useful plugin"
icon = "icon.svg"
repository = "https://github.com/lapce"
wasm = "lapce.wasm"
color-themes = [
"Dark.toml",
"Light.toml",
]
icon-themes = [
"Dark.svg",
"Light.svg",
]
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="100"
height="100"
viewBox="0 0 26.458333 26.458333"
version="1.1"
id="svg169"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
sodipodi:docname="рисунок1.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview171"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="5.9651515"
inkscape:cx="41.490983"
inkscape:cy="49.286259"
inkscape:window-width="1920"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs166" />
<g
inkscape:label="Слой 1"
inkscape:groupmode="layer"
id="layer1">
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke:#0000ff;stroke-width:2.0673;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path483"
cx="13.173385"
cy="13.48387"
rx="12.114121"
ry="11.892347" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,16 @@
name = "some-useful-plugin"
version = "0.1.56"
display-name = "Some Useful Plugin Name"
author = "some_author."
description = "very useful plugin"
icon = "icon.svg"
repository = "https://github.com/lapce"
wasm = "lapce.wasm"
color-themes = [
"Dark.toml",
"Light.toml",
]
icon-themes = [
"Dark.svg",
"Light.svg",
]
+237
View File
@@ -0,0 +1,237 @@
use std::collections::HashMap;
use lapce_rpc::plugin::VoltMetadata;
use serde_json::{Value, json};
use super::{load_volt, unflatten_map};
#[test]
fn test_unflatten_map() {
let map: HashMap<String, Value> = serde_json::from_value(json!({
"a.b.c": "d",
"a.d": ["e"],
}))
.unwrap();
assert_eq!(
unflatten_map(&map),
json!({
"a": {
"b": {
"c": "d",
},
"d": ["e"],
}
})
);
}
#[test]
fn test_load_volt() {
let lapce_proxy_dir = std::env::current_dir()
.expect("Can't get \"lapce-proxy\" directory")
.join("src")
.join("plugin")
.join("wasi")
.join("plugins");
// Invalid path (file does not exist)
let path = lapce_proxy_dir.join("some-path");
match path.canonicalize() {
Ok(path) => panic!("{path:?} file must not exast, but it is"),
Err(err) => assert_eq!(err.kind(), std::io::ErrorKind::NotFound),
};
// This should return Err since the file does not exist
if let Ok(volt_metadata) = load_volt(&lapce_proxy_dir) {
panic!(
"Unexpected result from `lapce_proxy::plugin::wasi::load_volt` function: {volt_metadata:?}"
);
}
// Invalid file (not readable into a string)
// Making sure the file exists
let path = lapce_proxy_dir.join("smiley.png");
let path = match path.canonicalize() {
Ok(path) => path,
Err(err) => panic!("{path:?} file must exast, but: {err:?}"),
};
// Making sure the data in the file is invalid utf-8
match std::fs::read_to_string(path.clone()) {
Ok(str) => panic!(
"{path:?} file must be invalid utf-8, but it is valid utf-8: {str:?}",
),
Err(err) => assert_eq!(err.kind(), std::io::ErrorKind::InvalidData),
}
// This should return Err since the `*.png` file cannot be read as a String
if let Ok(volt_metadata) = load_volt(&path) {
panic!(
"Unexpected result from `lapce_proxy::plugin::wasi::load_volt` function: {volt_metadata:?}",
);
}
// Invalid data in file (cannot be read as VoltMetadata)
// Making sure the file exists
let path = lapce_proxy_dir
.join("some_author.test-plugin-one")
.join("Light.svg");
let path = match path.canonicalize() {
Ok(path) => path,
Err(err) => panic!("{path:?} file must exast, but: {err:?}"),
};
// Making sure the data in the file is valid utf-8 (*.svg file is must be a valid utf-8)
match std::fs::read_to_string(path.clone()) {
Ok(_) => {}
Err(err) => panic!("{path:?} file must be valid utf-8, but {err:?}"),
}
// This should return Err since the data in the file cannot be interpreted as VoltMetadata
if let Ok(volt_metadata) = load_volt(&path) {
panic!(
"Unexpected result from `lapce_proxy::plugin::wasi::load_volt` function: {volt_metadata:?}",
);
}
let parent_path = lapce_proxy_dir.join("some_author.test-plugin-one");
let volt_metadata = match load_volt(&parent_path) {
Ok(volt_metadata) => volt_metadata,
Err(error) => panic!("{}", error),
};
let wasm_path = parent_path
.join("lapce.wasm")
.canonicalize()
.ok()
.as_ref()
.and_then(|path| path.to_str())
.map(ToOwned::to_owned);
let color_themes_paths = ["Dark.toml", "Light.toml"]
.into_iter()
.filter_map(|theme| {
parent_path
.join(theme)
.canonicalize()
.ok()
.as_ref()
.and_then(|path| path.to_str())
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>();
let icon_themes_paths = ["Dark.svg", "Light.svg"]
.into_iter()
.filter_map(|theme| {
parent_path
.join(theme)
.canonicalize()
.ok()
.as_ref()
.and_then(|path| path.to_str())
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>();
assert_eq!(
volt_metadata,
VoltMetadata {
name: "some-useful-plugin".to_string(),
version: "0.1.56".to_string(),
display_name: "Some Useful Plugin Name".to_string(),
author: "some_author".to_string(),
description: "very useful plugin".to_string(),
icon: Some("icon.svg".to_string()),
repository: Some("https://github.com/lapce".to_string()),
wasm: wasm_path,
color_themes: Some(color_themes_paths),
icon_themes: Some(icon_themes_paths),
dir: parent_path.canonicalize().ok(),
activation: None,
config: None
}
);
let parent_path = lapce_proxy_dir.join("some_author.test-plugin-two");
let volt_metadata = match load_volt(&parent_path) {
Ok(volt_metadata) => volt_metadata,
Err(error) => panic!("{}", error),
};
let wasm_path = parent_path
.join("lapce.wasm")
.canonicalize()
.ok()
.as_ref()
.and_then(|path| path.to_str())
.map(ToOwned::to_owned);
let color_themes_paths = ["Light.toml"]
.into_iter()
.filter_map(|theme| {
parent_path
.join(theme)
.canonicalize()
.ok()
.as_ref()
.and_then(|path| path.to_str())
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>();
let icon_themes_paths = ["Light.svg"]
.into_iter()
.filter_map(|theme| {
parent_path
.join(theme)
.canonicalize()
.ok()
.as_ref()
.and_then(|path| path.to_str())
.map(ToOwned::to_owned)
})
.collect::<Vec<_>>();
assert_eq!(
volt_metadata,
VoltMetadata {
name: "some-useful-plugin".to_string(),
version: "0.1.56".to_string(),
display_name: "Some Useful Plugin Name".to_string(),
author: "some_author.".to_string(),
description: "very useful plugin".to_string(),
icon: Some("icon.svg".to_string()),
repository: Some("https://github.com/lapce".to_string()),
wasm: wasm_path,
color_themes: Some(color_themes_paths),
icon_themes: Some(icon_themes_paths),
dir: parent_path.canonicalize().ok(),
activation: None,
config: None
}
);
let parent_path = lapce_proxy_dir.join("some_author.test-plugin-three");
let volt_metadata = match load_volt(&parent_path) {
Ok(volt_metadata) => volt_metadata,
Err(error) => panic!("{}", error),
};
assert_eq!(
volt_metadata,
VoltMetadata {
name: "some-useful-plugin".to_string(),
version: "0.1.56".to_string(),
display_name: "Some Useful Plugin Name".to_string(),
author: "some_author".to_string(),
description: "very useful plugin".to_string(),
icon: Some("icon.svg".to_string()),
repository: Some("https://github.com/lapce".to_string()),
wasm: None,
color_themes: Some(Vec::new()),
icon_themes: Some(Vec::new()),
dir: parent_path.canonicalize().ok(),
activation: None,
config: None
}
);
}
+384
View File
@@ -0,0 +1,384 @@
use std::{
borrow::Cow,
collections::VecDeque,
io::{self, ErrorKind, Read, Write},
num::NonZeroUsize,
path::PathBuf,
sync::Arc,
time::Duration,
};
use alacritty_terminal::{
event::{OnResize, WindowSize},
event_loop::Msg,
tty::{self, EventedPty, EventedReadWrite, Options, Shell, setup_env},
};
use anyhow::Result;
use crossbeam_channel::{Receiver, Sender};
use directories::BaseDirs;
use lapce_rpc::{
core::CoreRpcHandler,
terminal::{TermId, TerminalProfile},
};
use polling::PollMode;
const READ_BUFFER_SIZE: usize = 0x10_0000;
#[cfg(any(target_os = "linux", target_os = "macos"))]
const PTY_READ_WRITE_TOKEN: usize = 0;
#[cfg(any(target_os = "linux", target_os = "macos"))]
const PTY_CHILD_EVENT_TOKEN: usize = 1;
#[cfg(target_os = "windows")]
const PTY_READ_WRITE_TOKEN: usize = 2;
#[cfg(target_os = "windows")]
const PTY_CHILD_EVENT_TOKEN: usize = 1;
pub struct TerminalSender {
tx: Sender<Msg>,
poller: Arc<polling::Poller>,
}
impl TerminalSender {
pub fn new(tx: Sender<Msg>, poller: Arc<polling::Poller>) -> Self {
Self { tx, poller }
}
pub fn send(&self, msg: Msg) {
if let Err(err) = self.tx.send(msg) {
tracing::error!("{:?}", err);
}
if let Err(err) = self.poller.notify() {
tracing::error!("{:?}", err);
}
}
}
pub struct Terminal {
term_id: TermId,
pub(crate) poller: Arc<polling::Poller>,
pub(crate) pty: alacritty_terminal::tty::Pty,
rx: Receiver<Msg>,
pub tx: Sender<Msg>,
}
impl Terminal {
pub fn new(
term_id: TermId,
profile: TerminalProfile,
width: usize,
height: usize,
) -> Result<Terminal> {
let poll = polling::Poller::new()?.into();
let options = Options {
shell: Terminal::program(&profile),
working_directory: Terminal::workdir(&profile),
hold: false,
env: profile.environment.unwrap_or_default(),
};
setup_env();
#[cfg(target_os = "macos")]
set_locale_environment();
let size = WindowSize {
num_lines: height as u16,
num_cols: width as u16,
cell_width: 1,
cell_height: 1,
};
let pty = alacritty_terminal::tty::new(&options, size, 0)?;
let (tx, rx) = crossbeam_channel::unbounded();
Ok(Terminal {
term_id,
poller: poll,
pty,
tx,
rx,
})
}
pub fn run(&mut self, core_rpc: CoreRpcHandler) {
let mut state = State::default();
let mut buf = [0u8; READ_BUFFER_SIZE];
let poll_opts = PollMode::Level;
let mut interest = polling::Event::readable(0);
// Register TTY through EventedRW interface.
unsafe {
self.pty
.register(&self.poller, interest, poll_opts)
.unwrap();
}
let mut events =
polling::Events::with_capacity(NonZeroUsize::new(1024).unwrap());
let timeout = Some(Duration::from_secs(6));
let mut exit_code = None;
'event_loop: loop {
events.clear();
if let Err(err) = self.poller.wait(&mut events, timeout) {
match err.kind() {
ErrorKind::Interrupted => continue,
_ => panic!("EventLoop polling error: {err:?}"),
}
}
// Handle channel events, if there are any.
if !self.drain_recv_channel(&mut state) {
break;
}
for event in events.iter() {
match event.key {
PTY_CHILD_EVENT_TOKEN => {
if let Some(tty::ChildEvent::Exited(exited_code)) =
self.pty.next_child_event()
{
if let Err(err) = self.pty_read(&core_rpc, &mut buf) {
tracing::error!("{:?}", err);
}
exit_code = exited_code;
break 'event_loop;
}
}
PTY_READ_WRITE_TOKEN => {
if event.is_interrupt() {
// Don't try to do I/O on a dead PTY.
continue;
}
if event.readable {
if let Err(err) = self.pty_read(&core_rpc, &mut buf) {
// On Linux, a `read` on the master side of a PTY can fail
// with `EIO` if the client side hangs up. In that case,
// just loop back round for the inevitable `Exited` event.
// This sucks, but checking the process is either racy or
// blocking.
#[cfg(target_os = "linux")]
if err.raw_os_error() == Some(libc::EIO) {
continue;
}
tracing::error!(
"Error reading from PTY in event loop: {}",
err
);
break 'event_loop;
}
}
if event.writable {
if let Err(_err) = self.pty_write(&mut state) {
// error!(
// "Error writing to PTY in event loop: {}",
// err
// );
break 'event_loop;
}
}
}
_ => (),
}
}
// Register write interest if necessary.
let needs_write = state.needs_write();
if needs_write != interest.writable {
interest.writable = needs_write;
// Re-register with new interest.
self.pty
.reregister(&self.poller, interest, poll_opts)
.unwrap();
}
}
core_rpc.terminal_process_stopped(self.term_id, exit_code);
if let Err(err) = self.pty.deregister(&self.poller) {
tracing::error!("{:?}", err);
}
}
/// Drain the channel.
///
/// Returns `false` when a shutdown message was received.
fn drain_recv_channel(&mut self, state: &mut State) -> bool {
while let Ok(msg) = self.rx.try_recv() {
match msg {
Msg::Input(input) => state.write_list.push_back(input),
Msg::Shutdown => return false,
Msg::Resize(size) => self.pty.on_resize(size),
}
}
true
}
#[inline]
fn pty_read(
&mut self,
core_rpc: &CoreRpcHandler,
buf: &mut [u8],
) -> io::Result<()> {
loop {
match self.pty.reader().read(buf) {
Ok(0) => break,
Ok(n) => {
core_rpc.update_terminal(self.term_id, buf[..n].to_vec());
}
Err(err) => match err.kind() {
ErrorKind::Interrupted | ErrorKind::WouldBlock => {
break;
}
_ => return Err(err),
},
}
}
Ok(())
}
#[inline]
fn pty_write(&mut self, state: &mut State) -> io::Result<()> {
state.ensure_next();
'write_many: while let Some(mut current) = state.take_current() {
'write_one: loop {
match self.pty.writer().write(current.remaining_bytes()) {
Ok(0) => {
state.set_current(Some(current));
break 'write_many;
}
Ok(n) => {
current.advance(n);
if current.finished() {
state.goto_next();
break 'write_one;
}
}
Err(err) => {
state.set_current(Some(current));
match err.kind() {
ErrorKind::Interrupted | ErrorKind::WouldBlock => {
break 'write_many;
}
_ => return Err(err),
}
}
}
}
}
Ok(())
}
fn workdir(profile: &TerminalProfile) -> Option<PathBuf> {
if let Some(cwd) = &profile.workdir {
match cwd.to_file_path() {
Ok(cwd) => {
if cwd.exists() {
return Some(cwd);
}
}
Err(err) => {
tracing::error!("{:?}", err);
}
}
}
BaseDirs::new().map(|d| PathBuf::from(d.home_dir()))
}
fn program(profile: &TerminalProfile) -> Option<Shell> {
if let Some(command) = &profile.command {
if let Some(arguments) = &profile.arguments {
Some(Shell::new(command.to_owned(), arguments.to_owned()))
} else {
Some(Shell::new(command.to_owned(), Vec::new()))
}
} else {
None
}
}
}
struct Writing {
source: Cow<'static, [u8]>,
written: usize,
}
impl Writing {
#[inline]
fn new(c: Cow<'static, [u8]>) -> Writing {
Writing {
source: c,
written: 0,
}
}
#[inline]
fn advance(&mut self, n: usize) {
self.written += n;
}
#[inline]
fn remaining_bytes(&self) -> &[u8] {
&self.source[self.written..]
}
#[inline]
fn finished(&self) -> bool {
self.written >= self.source.len()
}
}
#[derive(Default)]
pub struct State {
write_list: VecDeque<Cow<'static, [u8]>>,
writing: Option<Writing>,
}
impl State {
#[inline]
fn ensure_next(&mut self) {
if self.writing.is_none() {
self.goto_next();
}
}
#[inline]
fn goto_next(&mut self) {
self.writing = self.write_list.pop_front().map(Writing::new);
}
#[inline]
fn take_current(&mut self) -> Option<Writing> {
self.writing.take()
}
#[inline]
fn needs_write(&self) -> bool {
self.writing.is_some() || !self.write_list.is_empty()
}
#[inline]
fn set_current(&mut self, new: Option<Writing>) {
self.writing = new;
}
}
#[cfg(target_os = "macos")]
fn set_locale_environment() {
let locale = locale_config::Locale::global_default()
.to_string()
.replace('-', "_");
unsafe {
std::env::set_var("LC_ALL", locale + ".UTF-8");
}
}
+275
View File
@@ -0,0 +1,275 @@
use std::{
collections::VecDeque,
path::{Path, PathBuf},
sync::Arc,
};
use crossbeam_channel::{Receiver, unbounded};
use notify::{
Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher,
event::{ModifyKind, RenameMode},
recommended_watcher,
};
use parking_lot::Mutex;
/// Wrapper around a `notify::Watcher`. It runs the inner watcher
/// in a separate thread, and communicates with it via a [crossbeam channel].
/// [crossbeam channel]: https://docs.rs/crossbeam-channel
pub struct FileWatcher {
rx_event: Option<Receiver<Result<Event, notify::Error>>>,
inner: RecommendedWatcher,
state: Arc<Mutex<WatcherState>>,
}
#[derive(Debug, Default)]
struct WatcherState {
events: EventQueue,
watchees: Vec<Watchee>,
}
/// Tracks a registered 'that-which-is-watched'.
#[doc(hidden)]
struct Watchee {
path: PathBuf,
recursive: bool,
token: WatchToken,
filter: Option<Box<PathFilter>>,
}
/// Token provided to `FileWatcher`, to associate events with
/// interested parties.
///
/// Note: `WatchToken`s are assumed to correspond with an
/// 'area of interest'; that is, they are used to route delivery
/// of events.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct WatchToken(pub usize);
/// A trait for types which can be notified of new events.
/// New events are accessible through the `FileWatcher` instance.
pub trait Notify: Send {
fn notify(&self, events: Vec<(WatchToken, Event)>);
}
pub type EventQueue = VecDeque<(WatchToken, Event)>;
pub type PathFilter = dyn Fn(&Path) -> bool + Send + 'static;
impl FileWatcher {
pub fn new() -> Self {
let (tx_event, rx_event) = unbounded();
let state = Arc::new(Mutex::new(WatcherState::default()));
let inner = recommended_watcher(tx_event).expect("watcher should spawn");
FileWatcher {
rx_event: Some(rx_event),
inner,
state,
}
}
pub fn notify<T: Notify + 'static>(&mut self, peer: T) {
let rx_event = self.rx_event.take().unwrap();
let state = self.state.clone();
std::thread::spawn(move || {
while let Ok(Ok(event)) = rx_event.recv() {
let mut events = Vec::new();
{
let mut state = state.lock();
let WatcherState {
ref mut watchees, ..
} = *state;
watchees
.iter()
.filter(|w| w.wants_event(&event))
.map(|w| w.token)
.for_each(|t| events.push((t, event.clone())));
}
peer.notify(events);
}
});
}
/// Begin watching `path`. As `Event`s (documented in the
/// [notify](https://docs.rs/notify) crate) arrive, they are stored
/// with the associated `token` and a task is added to the runloop's
/// idle queue.
///
/// Delivery of events then requires that the runloop's handler
/// correctly forward the `handle_idle` call to the interested party.
pub fn watch(&mut self, path: &Path, recursive: bool, token: WatchToken) {
self.watch_impl(path, recursive, token, None);
}
/// Like `watch`, but taking a predicate function that filters delivery
/// of events based on their path.
pub fn watch_filtered<F>(
&mut self,
path: &Path,
recursive: bool,
token: WatchToken,
filter: F,
) where
F: Fn(&Path) -> bool + Send + 'static,
{
let filter = Box::new(filter) as Box<PathFilter>;
self.watch_impl(path, recursive, token, Some(filter));
}
fn watch_impl(
&mut self,
path: &Path,
recursive: bool,
token: WatchToken,
filter: Option<Box<PathFilter>>,
) {
let path = match path.canonicalize() {
Ok(ref p) => p.to_owned(),
Err(_) => {
return;
}
};
let mut state = self.state.lock();
let w = Watchee {
path,
recursive,
token,
filter,
};
let mode = mode_from_bool(w.recursive);
if !state.watchees.iter().any(|w2| w.path == w2.path) {
if let Err(err) = self.inner.watch(&w.path, mode) {
tracing::error!("{:?}", err);
}
}
state.watchees.push(w);
}
/// Removes the provided token/path pair from the watch list.
/// Does not stop watching this path, if it is associated with
/// other tokens.
pub fn unwatch(&mut self, path: &Path, token: WatchToken) {
let mut state = self.state.lock();
let idx = state
.watchees
.iter()
.position(|w| w.token == token && w.path == path);
if let Some(idx) = idx {
let removed = state.watchees.remove(idx);
if !state.watchees.iter().any(|w| w.path == removed.path) {
if let Err(err) = self.inner.unwatch(&removed.path) {
tracing::error!("{:?}", err);
}
}
//TODO: Ideally we would be tracking what paths we're watching with
// some prefix-tree-like structure, which would let us keep track
// of when some child path might need to be reregistered. How this
// works and when registration would be required is dependent on
// the underlying notification mechanism, however. There's an
// in-progress rewrite of the Notify crate which use under the
// hood, and a component of that rewrite is adding this
// functionality; so until that lands we're using a fairly coarse
// heuristic to determine if we need to re-watch subpaths.
// if this was recursive, check if any child paths need to be
// manually re-added
if removed.recursive {
// do this in two steps because we've borrowed mutably up top
let to_add = state
.watchees
.iter()
.filter(|w| w.path.starts_with(&removed.path))
.map(|w| (w.path.to_owned(), mode_from_bool(w.recursive)))
.collect::<Vec<_>>();
for (path, mode) in to_add {
if let Err(err) = self.inner.watch(&path, mode) {
tracing::error!("{:?}", err);
}
}
}
}
}
/// Takes ownership of this `Watcher`'s current event queue.
pub fn take_events(&self) -> VecDeque<(WatchToken, Event)> {
let mut state = self.state.lock();
let WatcherState { ref mut events, .. } = *state;
std::mem::take(events)
}
}
impl Default for FileWatcher {
fn default() -> Self {
Self::new()
}
}
impl Watchee {
fn wants_event(&self, event: &Event) -> bool {
match &event.kind {
EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => {
if event.paths.len() == 2 {
//There will be two paths. First is "from" and other is "to".
self.applies_to_path(&event.paths[0])
|| self.applies_to_path(&event.paths[1])
} else {
false
}
}
EventKind::Create(_) | EventKind::Remove(_) | EventKind::Modify(_) => {
if event.paths.len() == 1 {
self.applies_to_path(&event.paths[0])
} else {
false
}
}
_ => false,
}
}
fn applies_to_path(&self, path: &Path) -> bool {
let general_case = if path.starts_with(&self.path) {
(self.recursive || self.path == path)
|| path.parent() == Some(self.path.as_path())
} else {
false
};
if let Some(ref filter) = self.filter {
general_case && filter(path)
} else {
general_case
}
}
}
impl std::fmt::Debug for Watchee {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"Watchee path: {:?}, r {}, t {} f {}",
self.path,
self.recursive,
self.token.0,
self.filter.is_some()
)
}
}
fn mode_from_bool(is_recursive: bool) -> RecursiveMode {
if is_recursive {
RecursiveMode::Recursive
} else {
RecursiveMode::NonRecursive
}
}