chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
use lapce_proxy::mainloop;
|
||||
|
||||
fn main() {
|
||||
mainloop();
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
@@ -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(¤t_path);
|
||||
for path in paths {
|
||||
if exedir == path.canonicalize()? {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let paths = std::env::split_paths(¤t_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |_| {});
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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",
|
||||
]
|
||||
@@ -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
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user