Text change generation, RPC call handling.
This commit is contained in:
parent
af1924404a
commit
cc6bdf8f66
7 changed files with 221 additions and 73 deletions
|
@ -27,4 +27,4 @@ pub use diagnostic::Diagnostic;
|
||||||
pub use history::History;
|
pub use history::History;
|
||||||
pub use state::State;
|
pub use state::State;
|
||||||
|
|
||||||
pub use transaction::{Assoc, Change, ChangeSet, Transaction};
|
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};
|
||||||
|
|
|
@ -5,8 +5,9 @@ use std::convert::TryFrom;
|
||||||
/// (from, to, replacement)
|
/// (from, to, replacement)
|
||||||
pub type Change = (usize, usize, Option<Tendril>);
|
pub type Change = (usize, usize, Option<Tendril>);
|
||||||
|
|
||||||
|
// TODO: pub(crate)
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(crate) enum Operation {
|
pub enum Operation {
|
||||||
/// Move cursor by n characters.
|
/// Move cursor by n characters.
|
||||||
Retain(usize),
|
Retain(usize),
|
||||||
/// Delete n characters.
|
/// Delete n characters.
|
||||||
|
@ -40,6 +41,12 @@ impl ChangeSet {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: from iter
|
// TODO: from iter
|
||||||
|
//
|
||||||
|
|
||||||
|
#[doc(hidden)] // used by lsp to convert to LSP changes
|
||||||
|
pub fn changes(&self) -> &[Operation] {
|
||||||
|
&self.changes
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn len_after(&self) -> usize {
|
fn len_after(&self) -> usize {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
transport::{Payload, Transport},
|
transport::{Payload, Transport},
|
||||||
Error, Notification,
|
Call, Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, Error>;
|
type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
use helix_core::{State, Transaction};
|
use helix_core::{ChangeSet, Transaction};
|
||||||
use helix_view::Document;
|
use helix_view::Document;
|
||||||
|
|
||||||
// use std::collections::HashMap;
|
// use std::collections::HashMap;
|
||||||
|
@ -27,7 +27,7 @@ pub struct Client {
|
||||||
stderr: BufReader<ChildStderr>,
|
stderr: BufReader<ChildStderr>,
|
||||||
|
|
||||||
outgoing: Sender<Payload>,
|
outgoing: Sender<Payload>,
|
||||||
pub incoming: Receiver<Notification>,
|
pub incoming: Receiver<Call>,
|
||||||
|
|
||||||
pub request_counter: u64,
|
pub request_counter: u64,
|
||||||
|
|
||||||
|
@ -87,6 +87,7 @@ impl Client {
|
||||||
Ok(params)
|
Ok(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Execute a RPC request on the language server.
|
||||||
pub async fn request<R: lsp::request::Request>(
|
pub async fn request<R: lsp::request::Request>(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: R::Params,
|
params: R::Params,
|
||||||
|
@ -126,6 +127,7 @@ impl Client {
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Send a RPC notification to the language server.
|
||||||
pub async fn notify<R: lsp::notification::Notification>(
|
pub async fn notify<R: lsp::notification::Notification>(
|
||||||
&mut self,
|
&mut self,
|
||||||
params: R::Params,
|
params: R::Params,
|
||||||
|
@ -149,6 +151,35 @@ impl Client {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Reply to a language server RPC call.
|
||||||
|
pub async fn reply(
|
||||||
|
&mut self,
|
||||||
|
id: jsonrpc::Id,
|
||||||
|
result: core::result::Result<Value, jsonrpc::Error>,
|
||||||
|
) -> Result<()> {
|
||||||
|
use jsonrpc::{Failure, Output, Success, Version};
|
||||||
|
|
||||||
|
let output = match result {
|
||||||
|
Ok(result) => Output::Success(Success {
|
||||||
|
jsonrpc: Some(Version::V2),
|
||||||
|
id,
|
||||||
|
result,
|
||||||
|
}),
|
||||||
|
Err(error) => Output::Failure(Failure {
|
||||||
|
jsonrpc: Some(Version::V2),
|
||||||
|
id,
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.outgoing
|
||||||
|
.send(Payload::Response(output))
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::Other(e.into()))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------
|
||||||
// General messages
|
// General messages
|
||||||
// -------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------
|
||||||
|
@ -163,7 +194,9 @@ impl Client {
|
||||||
// root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
|
// root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
|
||||||
root_uri: None, // set to project root in the future
|
root_uri: None, // set to project root in the future
|
||||||
initialization_options: None,
|
initialization_options: None,
|
||||||
capabilities: lsp::ClientCapabilities::default(),
|
capabilities: lsp::ClientCapabilities {
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
trace: None,
|
trace: None,
|
||||||
workspace_folders: None,
|
workspace_folders: None,
|
||||||
client_info: None,
|
client_info: None,
|
||||||
|
@ -203,23 +236,107 @@ impl Client {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_changes(changeset: &ChangeSet) -> Vec<lsp::TextDocumentContentChangeEvent> {
|
||||||
|
let mut iter = changeset.changes().iter().peekable();
|
||||||
|
let mut old_pos = 0;
|
||||||
|
|
||||||
|
let mut changes = Vec::new();
|
||||||
|
|
||||||
|
use crate::util::pos_to_lsp_pos;
|
||||||
|
use helix_core::Operation::*;
|
||||||
|
|
||||||
|
// TEMP
|
||||||
|
let rope = helix_core::Rope::from("");
|
||||||
|
let old_text = rope.slice(..);
|
||||||
|
|
||||||
|
while let Some(change) = iter.next() {
|
||||||
|
let len = match change {
|
||||||
|
Delete(i) | Retain(i) => *i,
|
||||||
|
Insert(_) => 0,
|
||||||
|
};
|
||||||
|
let old_end = old_pos + len;
|
||||||
|
|
||||||
|
match change {
|
||||||
|
Retain(_) => {}
|
||||||
|
Delete(_) => {
|
||||||
|
let start = pos_to_lsp_pos(&old_text, old_pos);
|
||||||
|
let end = pos_to_lsp_pos(&old_text, old_end);
|
||||||
|
|
||||||
|
// a subsequent ins means a replace, consume it
|
||||||
|
if let Some(Insert(s)) = iter.peek() {
|
||||||
|
iter.next();
|
||||||
|
|
||||||
|
// replacement
|
||||||
|
changes.push(lsp::TextDocumentContentChangeEvent {
|
||||||
|
range: Some(lsp::Range::new(start, end)),
|
||||||
|
text: s.into(),
|
||||||
|
range_length: None,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// deletion
|
||||||
|
changes.push(lsp::TextDocumentContentChangeEvent {
|
||||||
|
range: Some(lsp::Range::new(start, end)),
|
||||||
|
text: "".to_string(),
|
||||||
|
range_length: None,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Insert(s) => {
|
||||||
|
let start = pos_to_lsp_pos(&old_text, old_pos);
|
||||||
|
|
||||||
|
// insert
|
||||||
|
changes.push(lsp::TextDocumentContentChangeEvent {
|
||||||
|
range: Some(lsp::Range::new(start, start)),
|
||||||
|
text: s.into(),
|
||||||
|
range_length: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
old_pos = old_end;
|
||||||
|
}
|
||||||
|
|
||||||
|
changes
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: trigger any time history.commit_revision happens
|
// TODO: trigger any time history.commit_revision happens
|
||||||
pub async fn text_document_did_change(
|
pub async fn text_document_did_change(
|
||||||
&mut self,
|
&mut self,
|
||||||
doc: &Document,
|
doc: &Document,
|
||||||
transaction: &Transaction,
|
transaction: &Transaction,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
|
// figure out what kind of sync the server supports
|
||||||
|
|
||||||
|
let capabilities = self.capabilities.as_ref().unwrap(); // TODO: needs post init
|
||||||
|
|
||||||
|
let sync_capabilities = match capabilities.text_document_sync {
|
||||||
|
Some(lsp::TextDocumentSyncCapability::Kind(kind)) => kind,
|
||||||
|
Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
|
||||||
|
change: Some(kind),
|
||||||
|
..
|
||||||
|
})) => kind,
|
||||||
|
// None | SyncOptions { changes: None }
|
||||||
|
_ => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let changes = match sync_capabilities {
|
||||||
|
lsp::TextDocumentSyncKind::Full => {
|
||||||
|
vec![lsp::TextDocumentContentChangeEvent {
|
||||||
|
// range = None -> whole document
|
||||||
|
range: None, //Some(Range)
|
||||||
|
range_length: None, // u64 apparently deprecated
|
||||||
|
text: "".to_string(),
|
||||||
|
}] // TODO: probably need old_state here too?
|
||||||
|
}
|
||||||
|
lsp::TextDocumentSyncKind::Incremental => Self::to_changes(transaction.changes()),
|
||||||
|
lsp::TextDocumentSyncKind::None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
self.notify::<lsp::notification::DidChangeTextDocument>(lsp::DidChangeTextDocumentParams {
|
self.notify::<lsp::notification::DidChangeTextDocument>(lsp::DidChangeTextDocumentParams {
|
||||||
text_document: lsp::VersionedTextDocumentIdentifier::new(
|
text_document: lsp::VersionedTextDocumentIdentifier::new(
|
||||||
lsp::Url::from_file_path(doc.path().unwrap()).unwrap(),
|
lsp::Url::from_file_path(doc.path().unwrap()).unwrap(),
|
||||||
doc.version,
|
doc.version,
|
||||||
),
|
),
|
||||||
content_changes: vec![lsp::TextDocumentContentChangeEvent {
|
content_changes: changes,
|
||||||
// range = None -> whole document
|
|
||||||
range: None, //Some(Range)
|
|
||||||
range_length: None, // u64 apparently deprecated
|
|
||||||
text: "".to_string(),
|
|
||||||
}], // TODO: probably need old_state here too?
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
mod client;
|
mod client;
|
||||||
mod transport;
|
mod transport;
|
||||||
|
|
||||||
use jsonrpc_core as jsonrpc;
|
pub use jsonrpc_core as jsonrpc;
|
||||||
use lsp_types as lsp;
|
pub use lsp_types as lsp;
|
||||||
|
|
||||||
pub use client::Client;
|
pub use client::Client;
|
||||||
pub use lsp::{Position, Url};
|
pub use lsp::{Position, Url};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -30,19 +29,13 @@ pub mod util {
|
||||||
let line_start = doc.char_to_utf16_cu(line);
|
let line_start = doc.char_to_utf16_cu(line);
|
||||||
doc.utf16_cu_to_char(pos.character as usize + line_start)
|
doc.utf16_cu_to_char(pos.character as usize + line_start)
|
||||||
}
|
}
|
||||||
}
|
pub fn pos_to_lsp_pos(doc: &helix_core::RopeSlice, pos: usize) -> lsp::Position {
|
||||||
|
let line = doc.char_to_line(pos);
|
||||||
|
let line_start = doc.char_to_utf16_cu(line);
|
||||||
|
let col = doc.char_to_utf16_cu(pos) - line_start;
|
||||||
|
|
||||||
/// A type representing all possible values sent from the server to the client.
|
lsp::Position::new(line as u64, col as u64)
|
||||||
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
}
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum Message {
|
|
||||||
/// A regular JSON-RPC request output (single response).
|
|
||||||
Output(jsonrpc::Output),
|
|
||||||
/// A notification.
|
|
||||||
Notification(jsonrpc::Notification),
|
|
||||||
/// A JSON-RPC request
|
|
||||||
Call(jsonrpc::Call),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
|
@ -67,3 +60,5 @@ impl Notification {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub use jsonrpc::Call;
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::collections::HashMap;
|
||||||
|
|
||||||
use log::debug;
|
use log::debug;
|
||||||
|
|
||||||
use crate::{Error, Message, Notification};
|
use crate::{Error, Notification};
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, Error>;
|
type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|
||||||
|
@ -24,10 +24,23 @@ pub(crate) enum Payload {
|
||||||
value: jsonrpc::MethodCall,
|
value: jsonrpc::MethodCall,
|
||||||
},
|
},
|
||||||
Notification(jsonrpc::Notification),
|
Notification(jsonrpc::Notification),
|
||||||
|
Response(jsonrpc::Output),
|
||||||
|
}
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
/// A type representing all possible values sent from the server to the client.
|
||||||
|
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum Message {
|
||||||
|
/// A regular JSON-RPC request output (single response).
|
||||||
|
Output(jsonrpc::Output),
|
||||||
|
/// A JSON-RPC request or notification.
|
||||||
|
Call(jsonrpc::Call),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct Transport {
|
pub(crate) struct Transport {
|
||||||
incoming: Sender<Notification>, // TODO Notification | Call
|
incoming: Sender<jsonrpc::Call>,
|
||||||
outgoing: Receiver<Payload>,
|
outgoing: Receiver<Payload>,
|
||||||
|
|
||||||
pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
|
pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
|
||||||
|
@ -42,7 +55,7 @@ impl Transport {
|
||||||
ex: &Executor,
|
ex: &Executor,
|
||||||
reader: BufReader<ChildStdout>,
|
reader: BufReader<ChildStdout>,
|
||||||
writer: BufWriter<ChildStdin>,
|
writer: BufWriter<ChildStdin>,
|
||||||
) -> (Receiver<Notification>, Sender<Payload>) {
|
) -> (Receiver<jsonrpc::Call>, Sender<Payload>) {
|
||||||
let (incoming, rx) = smol::channel::unbounded();
|
let (incoming, rx) = smol::channel::unbounded();
|
||||||
let (tx, outgoing) = smol::channel::unbounded();
|
let (tx, outgoing) = smol::channel::unbounded();
|
||||||
|
|
||||||
|
@ -112,6 +125,10 @@ impl Transport {
|
||||||
let json = serde_json::to_string(&value)?;
|
let json = serde_json::to_string(&value)?;
|
||||||
self.send(json).await
|
self.send(json).await
|
||||||
}
|
}
|
||||||
|
Payload::Response(error) => {
|
||||||
|
let json = serde_json::to_string(&error)?;
|
||||||
|
self.send(json).await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,24 +148,18 @@ impl Transport {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> {
|
async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> {
|
||||||
match msg {
|
match msg {
|
||||||
Message::Output(output) => self.recv_response(output).await?,
|
Message::Output(output) => self.recv_response(output).await?,
|
||||||
Message::Notification(jsonrpc::Notification { method, params, .. }) => {
|
|
||||||
let notification = Notification::parse(&method, params);
|
|
||||||
|
|
||||||
debug!("<- {} {:?}", method, notification);
|
|
||||||
self.incoming.send(notification).await?;
|
|
||||||
}
|
|
||||||
Message::Call(call) => {
|
Message::Call(call) => {
|
||||||
debug!("<- {:?}", call);
|
self.incoming.send(call).await?;
|
||||||
// dispatch
|
// let notification = Notification::parse(&method, params);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recv_response(&mut self, output: jsonrpc::Output) -> anyhow::Result<()> {
|
async fn recv_response(&mut self, output: jsonrpc::Output) -> anyhow::Result<()> {
|
||||||
match output {
|
match output {
|
||||||
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
|
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
|
||||||
debug!("<- {}", result);
|
debug!("<- {}", result);
|
||||||
|
@ -191,6 +202,8 @@ impl Transport {
|
||||||
}
|
}
|
||||||
let msg = msg.unwrap();
|
let msg = msg.unwrap();
|
||||||
|
|
||||||
|
debug!("<- {:?}", msg);
|
||||||
|
|
||||||
self.recv_msg(msg).await.unwrap();
|
self.recv_msg(msg).await.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -433,8 +433,8 @@ impl<'a> Application<'a> {
|
||||||
event = reader.next().fuse() => {
|
event = reader.next().fuse() => {
|
||||||
self.handle_terminal_events(event).await
|
self.handle_terminal_events(event).await
|
||||||
}
|
}
|
||||||
notification = self.lsp.incoming.next().fuse() => {
|
call = self.lsp.incoming.next().fuse() => {
|
||||||
self.handle_lsp_notification(notification).await
|
self.handle_lsp_message(call).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -566,43 +566,56 @@ impl<'a> Application<'a> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_lsp_notification(&mut self, notification: Option<helix_lsp::Notification>) {
|
pub async fn handle_lsp_message(&mut self, call: Option<helix_lsp::Call>) {
|
||||||
use helix_lsp::Notification;
|
use helix_lsp::{Call, Notification};
|
||||||
match notification {
|
match call {
|
||||||
Some(Notification::PublishDiagnostics(params)) => {
|
Some(Call::Notification(helix_lsp::jsonrpc::Notification {
|
||||||
let path = Some(params.uri.to_file_path().unwrap());
|
method, params, ..
|
||||||
let view = self
|
})) => {
|
||||||
.editor
|
let notification = Notification::parse(&method, params);
|
||||||
.views
|
match notification {
|
||||||
.iter_mut()
|
Notification::PublishDiagnostics(params) => {
|
||||||
.find(|view| view.doc.path == path);
|
let path = Some(params.uri.to_file_path().unwrap());
|
||||||
|
let view = self
|
||||||
|
.editor
|
||||||
|
.views
|
||||||
|
.iter_mut()
|
||||||
|
.find(|view| view.doc.path == path);
|
||||||
|
|
||||||
if let Some(view) = view {
|
if let Some(view) = view {
|
||||||
let doc = view.doc.text().slice(..);
|
let doc = view.doc.text().slice(..);
|
||||||
let diagnostics = params
|
let diagnostics = params
|
||||||
.diagnostics
|
.diagnostics
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|diagnostic| {
|
.map(|diagnostic| {
|
||||||
use helix_lsp::util::lsp_pos_to_pos;
|
use helix_lsp::util::lsp_pos_to_pos;
|
||||||
let start = lsp_pos_to_pos(&doc, diagnostic.range.start);
|
let start = lsp_pos_to_pos(&doc, diagnostic.range.start);
|
||||||
let end = lsp_pos_to_pos(&doc, diagnostic.range.end);
|
let end = lsp_pos_to_pos(&doc, diagnostic.range.end);
|
||||||
|
|
||||||
helix_core::Diagnostic {
|
helix_core::Diagnostic {
|
||||||
range: (start, end),
|
range: (start, end),
|
||||||
line: diagnostic.range.start.line as usize,
|
line: diagnostic.range.start.line as usize,
|
||||||
message: diagnostic.message,
|
message: diagnostic.message,
|
||||||
// severity
|
// severity
|
||||||
// code
|
// code
|
||||||
// source
|
// source
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
view.doc.diagnostics = diagnostics;
|
view.doc.diagnostics = diagnostics;
|
||||||
|
|
||||||
self.render();
|
self.render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(Call::MethodCall(call)) => {
|
||||||
|
// TODO: need to make Result<Value, Error>
|
||||||
|
|
||||||
|
unimplemented!("{:?}", call)
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,9 @@ use std::collections::HashMap;
|
||||||
// = = align?
|
// = = align?
|
||||||
// + =
|
// + =
|
||||||
// }
|
// }
|
||||||
|
//
|
||||||
|
// gd = goto definition
|
||||||
|
// gr = goto reference
|
||||||
// }
|
// }
|
||||||
|
|
||||||
#[cfg(feature = "term")]
|
#[cfg(feature = "term")]
|
||||||
|
|
Loading…
Add table
Reference in a new issue