Reroute LSP notification events into the main app event loop.

This commit is contained in:
Blaž Hrastnik 2020-10-20 13:58:34 +09:00
parent 64b5b23315
commit f9bfba4d96
11 changed files with 206 additions and 154 deletions

1
Cargo.lock generated
View File

@ -472,6 +472,7 @@ dependencies = [
"anyhow",
"clap",
"crossterm",
"futures-util",
"helix-core",
"helix-lsp",
"helix-view",

View File

@ -0,0 +1 @@
pub struct Diagnostic {}

View File

@ -1,4 +1,5 @@
#![allow(unused)]
mod diagnostic;
pub mod graphemes;
mod history;
pub mod indent;
@ -22,6 +23,7 @@
pub use selection::Selection;
pub use syntax::Syntax;
pub use diagnostic::Diagnostic;
pub use history::History;
pub use state::State;

View File

@ -1,6 +1,6 @@
use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
use crate::syntax::LOADER;
use crate::{ChangeSet, Position, Range, Rope, RopeSlice, Selection, Syntax};
use crate::{ChangeSet, Diagnostic, Position, Range, Rope, RopeSlice, Selection, Syntax};
use anyhow::Error;
use std::path::PathBuf;
@ -28,6 +28,8 @@ pub struct State {
/// Pending changes since last history commit.
pub changes: ChangeSet,
pub old_state: Option<(Rope, Selection)>,
pub diagnostics: Vec<Diagnostic>,
}
#[derive(Copy, Clone, PartialEq, Eq)]
@ -58,12 +60,13 @@ pub fn new(doc: Rope) -> Self {
syntax: None,
changes,
old_state,
diagnostics: Vec::new(),
}
}
// TODO: passing scopes here is awkward
pub fn load(path: PathBuf, scopes: &[String]) -> Result<Self, Error> {
use std::{env, fs::File, io::BufReader, path::PathBuf};
use std::{env, fs::File, io::BufReader};
let _current_dir = env::current_dir()?;
let doc = Rope::from_reader(BufReader::new(File::open(path.clone())?))?;
@ -81,7 +84,8 @@ pub fn load(path: PathBuf, scopes: &[String]) -> Result<Self, Error> {
state.syntax = Some(syntax);
};
state.path = Some(path);
// canonicalize path to absolute value
state.path = Some(std::fs::canonicalize(path)?);
Ok(state)
}

View File

@ -2,7 +2,7 @@
use transport::{Payload, Transport};
use std::collections::HashMap;
// use std::collections::HashMap;
use jsonrpc_core as jsonrpc;
use lsp_types as lsp;
@ -32,10 +32,12 @@ enum Message {
}
#[derive(Debug, PartialEq, Clone)]
enum Notification {}
pub enum Notification {
PublishDiagnostics(lsp::PublishDiagnosticsParams),
}
impl Notification {
pub fn parse(method: &str, params: jsonrpc::Params) {
pub fn parse(method: &str, params: jsonrpc::Params) -> Notification {
use lsp::notification::Notification as _;
match method {
@ -44,11 +46,10 @@ pub fn parse(method: &str, params: jsonrpc::Params) {
.parse()
.expect("Failed to parse PublishDiagnostics params");
println!("{:?}", params);
// TODO: need to loop over diagnostics and distinguish them by URI
Notification::PublishDiagnostics(params)
}
_ => println!("unhandled notification: {}", method),
_ => unimplemented!("unhandled notification: {}", method),
}
}
}
@ -58,13 +59,13 @@ pub struct Client {
stderr: BufReader<ChildStderr>,
outgoing: Sender<Payload>,
incoming: Receiver<Message>,
pub incoming: Receiver<Notification>,
pub request_counter: u64,
capabilities: Option<lsp::ServerCapabilities>,
// TODO: handle PublishDiagnostics Version
diagnostics: HashMap<lsp::Url, Vec<lsp::Diagnostic>>,
// diagnostics: HashMap<lsp::Url, Vec<lsp::Diagnostic>>,
}
impl Client {
@ -95,7 +96,7 @@ pub fn start(ex: &Executor, cmd: &str, args: &[String]) -> Self {
request_counter: 0,
capabilities: None,
diagnostics: HashMap::new(),
// diagnostics: HashMap::new(),
}
}
@ -226,10 +227,7 @@ pub async fn text_document_did_open(
) -> anyhow::Result<()> {
self.notify::<lsp::notification::DidOpenTextDocument>(lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem {
uri: lsp::Url::from_file_path(
std::fs::canonicalize(state.path.as_ref().unwrap()).unwrap(),
)
.unwrap(),
uri: lsp::Url::from_file_path(state.path().unwrap()).unwrap(),
language_id: "rust".to_string(), // TODO: hardcoded for now
version: 0,
text: String::from(&state.doc),
@ -243,11 +241,12 @@ pub async fn text_document_did_change(
&mut self,
state: &helix_core::State,
) -> anyhow::Result<()> {
self.notify::<lsp::notification::DidSaveTextDocument>(lsp::DidSaveTextDocumentParams {
text_document: lsp::TextDocumentIdentifier::new(
lsp::Url::from_file_path(state.path.as_ref().unwrap()).unwrap(),
self.notify::<lsp::notification::DidChangeTextDocument>(lsp::DidChangeTextDocumentParams {
text_document: lsp::VersionedTextDocumentIdentifier::new(
lsp::Url::from_file_path(state.path().unwrap()).unwrap(),
0, // TODO: version
),
text: None, // TODO?
content_changes: vec![], // TODO:
})
.await
}

View File

@ -24,7 +24,7 @@ pub(crate) enum Payload {
}
pub(crate) struct Transport {
incoming: Sender<Message>,
incoming: Sender<Notification>, // TODO Notification | Call
outgoing: Receiver<Payload>,
pending_requests: HashMap<jsonrpc::Id, Sender<anyhow::Result<Value>>>,
@ -39,7 +39,7 @@ pub fn start(
ex: &Executor,
reader: BufReader<ChildStdout>,
writer: BufWriter<ChildStdin>,
) -> (Receiver<Message>, Sender<Payload>) {
) -> (Receiver<Notification>, Sender<Payload>) {
let (incoming, rx) = smol::channel::unbounded();
let (tx, outgoing) = smol::channel::unbounded();
@ -111,7 +111,7 @@ pub async fn send_payload(&mut self, payload: Payload) -> anyhow::Result<()> {
}
pub async fn send(&mut self, request: String) -> anyhow::Result<()> {
println!("-> {}", request);
// println!("-> {}", request);
// send the headers
self.writer
@ -132,11 +132,11 @@ pub async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> {
Message::Notification(jsonrpc::Notification { method, params, .. }) => {
let notification = Notification::parse(&method, params);
println!("<- {} {:?}", method, notification);
// dispatch
// println!("<- {} {:?}", method, notification);
self.incoming.send(notification).await?;
}
Message::Call(call) => {
println!("<- {:?}", call);
// println!("<- {:?}", call);
// dispatch
}
_ => unreachable!(),
@ -147,7 +147,7 @@ pub async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> {
pub async fn recv_response(&mut self, output: jsonrpc::Output) -> anyhow::Result<()> {
match output {
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
println!("<- {}", result);
// println!("<- {}", result);
let tx = self
.pending_requests

View File

@ -24,3 +24,5 @@ num_cpus = "1.13"
tui = { git = "https://github.com/fdehau/tui-rs", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.18", features = ["event-stream"] }
clap = { version = "3.0.0-beta.2 ", default-features = false, features = ["std", "cargo"] }
futures-util = "0.3"

View File

@ -1,6 +1,11 @@
use clap::ArgMatches as Args;
use helix_core::{indent::TAB_WIDTH, state::Mode, syntax::HighlightEvent, Position, Range, State};
use helix_view::{commands, keymap, prompt::Prompt, Editor, Theme, View};
use helix_view::{
commands,
keymap::{self, Keymaps},
prompt::Prompt,
Editor, Theme, View,
};
use std::{
borrow::Cow,
@ -31,14 +36,16 @@
type Terminal = tui::Terminal<CrosstermBackend<std::io::Stdout>>;
static EX: smol::Executor = smol::Executor::new();
const BASE_WIDTH: u16 = 30;
pub struct Application {
pub struct Application<'a> {
editor: Editor,
prompt: Option<Prompt>,
terminal: Renderer,
keymap: Keymaps,
executor: &'a smol::Executor<'a>,
lsp: helix_lsp::Client,
}
struct Renderer {
@ -235,7 +242,7 @@ pub fn render_statusline(&mut self, view: &View, theme: &Theme) {
.set_string(1, self.size.1 - 2, mode, self.text_color);
}
pub fn render_prompt(&mut self, view: &View, prompt: &Prompt) {
pub fn render_prompt(&mut self, view: &View, prompt: &Prompt, theme: &Theme) {
// completion
if !prompt.completion.is_empty() {
// TODO: find out better way of clearing individual lines of the screen
@ -254,7 +261,7 @@ pub fn render_prompt(&mut self, view: &View, prompt: &Prompt) {
}
self.surface.set_style(
Rect::new(0, self.size.1 - col_height - 2, self.size.0, col_height),
view.theme.get("ui.statusline"),
theme.get("ui.statusline"),
);
for (i, command) in prompt.completion.iter().enumerate() {
let color = if prompt.completion_selection_index.is_some()
@ -330,8 +337,8 @@ pub fn render_cursor(&mut self, view: &View, prompt: Option<&Prompt>, viewport:
}
}
impl Application {
pub fn new(mut args: Args) -> Result<Self, Error> {
impl<'a> Application<'a> {
pub fn new(mut args: Args, executor: &'a smol::Executor<'a>) -> Result<Self, Error> {
let terminal = Renderer::new()?;
let mut editor = Editor::new();
@ -339,11 +346,18 @@ pub fn new(mut args: Args) -> Result<Self, Error> {
editor.open(file, terminal.size)?;
}
let lsp = helix_lsp::Client::start(&executor, "rust-analyzer", &[]);
let mut app = Self {
editor,
terminal,
// TODO; move to state
prompt: None,
//
keymap: keymap::default(),
executor,
lsp,
};
Ok(app)
@ -361,7 +375,7 @@ fn render(&mut self) {
if prompt.should_close {
self.prompt = None;
} else {
self.terminal.render_prompt(view, prompt);
self.terminal.render_prompt(view, prompt, theme_ref);
}
}
}
@ -375,7 +389,13 @@ fn render(&mut self) {
pub async fn event_loop(&mut self) {
let mut reader = EventStream::new();
let keymap = keymap::default();
// initialize lsp
let res = self.lsp.initialize().await;
let res = self
.lsp
.text_document_did_open(&self.editor.view().unwrap().state)
.await;
self.render();
@ -384,126 +404,149 @@ pub async fn event_loop(&mut self) {
break;
}
// Handle key events
match reader.next().await {
Some(Ok(Event::Resize(width, height))) => {
self.terminal.resize(width, height);
use futures_util::{select, FutureExt};
select! {
event = reader.next().fuse() => {
self.handle_terminal_events(event).await
}
notification = self.lsp.incoming.next().fuse() => {
self.handle_lsp_notification(notification).await
}
}
}
}
// TODO: simplistic ensure cursor in view for now
// TODO: loop over views
if let Some(view) = self.editor.view_mut() {
view.size = self.terminal.size;
view.ensure_cursor_in_view()
};
pub async fn handle_terminal_events(
&mut self,
event: Option<Result<Event, crossterm::ErrorKind>>,
) {
// Handle key events
match event {
Some(Ok(Event::Resize(width, height))) => {
self.terminal.resize(width, height);
// TODO: simplistic ensure cursor in view for now
// TODO: loop over views
if let Some(view) = self.editor.view_mut() {
view.size = self.terminal.size;
view.ensure_cursor_in_view()
};
self.render();
}
Some(Ok(Event::Key(event))) => {
// if there's a prompt, it takes priority
if let Some(prompt) = &mut self.prompt {
self.prompt
.as_mut()
.unwrap()
.handle_input(event, &mut self.editor);
self.render();
}
Some(Ok(Event::Key(event))) => {
// if there's a prompt, it takes priority
if let Some(prompt) = &mut self.prompt {
self.prompt
.as_mut()
.unwrap()
.handle_input(event, &mut self.editor);
} else if let Some(view) = self.editor.view_mut() {
let keys = vec![event];
// TODO: sequences (`gg`)
// TODO: handle count other than 1
match view.state.mode() {
Mode::Insert => {
if let Some(command) = self.keymap[&Mode::Insert].get(&keys) {
command(view, 1);
} else if let KeyEvent {
code: KeyCode::Char(c),
..
} = event
{
commands::insert::insert_char(view, c);
}
view.ensure_cursor_in_view();
}
Mode::Normal => {
if let &[KeyEvent {
code: KeyCode::Char(':'),
..
}] = keys.as_slice()
{
let prompt = Prompt::new(
":".to_owned(),
|_input: &str| {
// TODO: i need this duplicate list right now to avoid borrow checker issues
let command_list = vec![
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("averylongcommandaverylongcommandaverylongcommandaverylongcommandaverylongcommand"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
];
command_list
.into_iter()
.filter(|command| command.contains(_input))
.collect()
}, // completion
|editor: &mut Editor, input: &str| match input {
"q" => editor.should_close = true,
_ => (),
},
);
self.render();
} else if let Some(view) = self.editor.view_mut() {
let keys = vec![event];
// TODO: sequences (`gg`)
// TODO: handle count other than 1
match view.state.mode() {
Mode::Insert => {
if let Some(command) = keymap[&Mode::Insert].get(&keys) {
command(view, 1);
} else if let KeyEvent {
code: KeyCode::Char(c),
..
} = event
{
commands::insert::insert_char(view, c);
}
self.prompt = Some(prompt);
// HAXX: special casing for command mode
} else if let Some(command) = self.keymap[&Mode::Normal].get(&keys) {
command(view, 1);
// TODO: simplistic ensure cursor in view for now
view.ensure_cursor_in_view();
}
Mode::Normal => {
if let &[KeyEvent {
code: KeyCode::Char(':'),
..
}] = keys.as_slice()
{
let prompt = Prompt::new(
":".to_owned(),
|_input: &str| {
// TODO: i need this duplicate list right now to avoid borrow checker issues
let command_list = vec![
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("averylongcommandaverylongcommandaverylongcommandaverylongcommandaverylongcommand"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
String::from("q"),
String::from("aaa"),
String::from("bbb"),
String::from("ccc"),
String::from("ddd"),
String::from("eee"),
];
command_list
.into_iter()
.filter(|command| command.contains(_input))
.collect()
}, // completion
|editor: &mut Editor, input: &str| match input {
"q" => editor.should_close = true,
_ => (),
},
);
}
mode => {
if let Some(command) = self.keymap[&mode].get(&keys) {
command(view, 1);
self.prompt = Some(prompt);
// HAXX: special casing for command mode
} else if let Some(command) = keymap[&Mode::Normal].get(&keys) {
command(view, 1);
// TODO: simplistic ensure cursor in view for now
view.ensure_cursor_in_view();
}
}
mode => {
if let Some(command) = keymap[&mode].get(&keys) {
command(view, 1);
// TODO: simplistic ensure cursor in view for now
view.ensure_cursor_in_view();
}
// TODO: simplistic ensure cursor in view for now
view.ensure_cursor_in_view();
}
}
self.render();
}
self.render();
}
Some(Ok(Event::Mouse(_))) => (), // unhandled
Some(Err(x)) => panic!(x),
None => break,
}
Some(Ok(Event::Mouse(_))) => (), // unhandled
Some(Err(x)) => panic!(x),
None => panic!(),
};
}
pub async fn handle_lsp_notification(&mut self, notification: Option<helix_lsp::Notification>) {
use helix_lsp::Notification;
match notification {
Some(Notification::PublishDiagnostics(params)) => unimplemented!("{:?}", params),
_ => unreachable!(),
}
}

View File

@ -26,15 +26,15 @@ fn main() -> Result<(), Error> {
std::thread::spawn(move || smol::block_on(EX.run(smol::future::pending::<()>())));
}
let mut lsp = helix_lsp::Client::start(&EX, "rust-analyzer", &[]);
// let mut lsp = helix_lsp::Client::start(&EX, "rust-analyzer", &[]);
smol::block_on(async {
let res = lsp.initialize().await;
let state = helix_core::State::load("test.rs".into(), &[]).unwrap();
let res = lsp.text_document_did_open(&state).await;
loop {}
// let res = lsp.initialize().await;
// let state = helix_core::State::load("test.rs".into(), &[]).unwrap();
// let res = lsp.text_document_did_open(&state).await;
// loop {}
// Application::new(args).unwrap().run().await;
Application::new(args, &EX).unwrap().run().await;
});
Ok(())

View File

@ -87,8 +87,8 @@
pub use crossterm::event::{KeyCode, KeyEvent as Key, KeyModifiers as Modifiers};
// TODO: could be trie based
type Keymap = HashMap<Vec<Key>, Command>;
type Keymaps = HashMap<state::Mode, Keymap>;
pub type Keymap = HashMap<Vec<Key>, Command>;
pub type Keymaps = HashMap<state::Mode, Keymap>;
macro_rules! key {
($ch:expr) => {

View File

@ -18,7 +18,7 @@ pub struct View {
pub first_line: usize,
pub size: (u16, u16),
// TODO: Doc<> fields
// TODO: Doc fields
pub history: History,
}