diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index 8458c36f..ddf1439c 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -27,4 +27,4 @@ pub use diagnostic::Diagnostic;
 pub use history::History;
 pub use state::State;
 
-pub use transaction::{Assoc, Change, ChangeSet, Transaction};
+pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index 9bd8c615..f1cb2ca1 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -5,8 +5,9 @@ use std::convert::TryFrom;
 /// (from, to, replacement)
 pub type Change = (usize, usize, Option<Tendril>);
 
+// TODO: pub(crate)
 #[derive(Debug, Clone, PartialEq, Eq)]
-pub(crate) enum Operation {
+pub enum Operation {
     /// Move cursor by n characters.
     Retain(usize),
     /// Delete n characters.
@@ -40,6 +41,12 @@ impl ChangeSet {
     }
 
     // TODO: from iter
+    //
+
+    #[doc(hidden)] // used by lsp to convert to LSP changes
+    pub fn changes(&self) -> &[Operation] {
+        &self.changes
+    }
 
     #[must_use]
     fn len_after(&self) -> usize {
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 56413768..3c2c1ce0 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -1,11 +1,11 @@
 use crate::{
     transport::{Payload, Transport},
-    Error, Notification,
+    Call, Error,
 };
 
 type Result<T> = core::result::Result<T, Error>;
 
-use helix_core::{State, Transaction};
+use helix_core::{ChangeSet, Transaction};
 use helix_view::Document;
 
 // use std::collections::HashMap;
@@ -27,7 +27,7 @@ pub struct Client {
     stderr: BufReader<ChildStderr>,
 
     outgoing: Sender<Payload>,
-    pub incoming: Receiver<Notification>,
+    pub incoming: Receiver<Call>,
 
     pub request_counter: u64,
 
@@ -87,6 +87,7 @@ impl Client {
         Ok(params)
     }
 
+    /// Execute a RPC request on the language server.
     pub async fn request<R: lsp::request::Request>(
         &mut self,
         params: R::Params,
@@ -126,6 +127,7 @@ impl Client {
         Ok(response)
     }
 
+    /// Send a RPC notification to the language server.
     pub async fn notify<R: lsp::notification::Notification>(
         &mut self,
         params: R::Params,
@@ -149,6 +151,35 @@ impl Client {
         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
     // -------------------------------------------------------------------------------------------
@@ -163,7 +194,9 @@ impl Client {
             // root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
             root_uri: None, // set to project root in the future
             initialization_options: None,
-            capabilities: lsp::ClientCapabilities::default(),
+            capabilities: lsp::ClientCapabilities {
+                ..Default::default()
+            },
             trace: None,
             workspace_folders: None,
             client_info: None,
@@ -203,23 +236,107 @@ impl Client {
         .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
     pub async fn text_document_did_change(
         &mut self,
         doc: &Document,
         transaction: &Transaction,
     ) -> 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 {
             text_document: lsp::VersionedTextDocumentIdentifier::new(
                 lsp::Url::from_file_path(doc.path().unwrap()).unwrap(),
                 doc.version,
             ),
-            content_changes: 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?
+            content_changes: changes,
         })
         .await
     }
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index c37222f1..1ee8199f 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -1,13 +1,12 @@
 mod client;
 mod transport;
 
-use jsonrpc_core as jsonrpc;
-use lsp_types as lsp;
+pub use jsonrpc_core as jsonrpc;
+pub use lsp_types as lsp;
 
 pub use client::Client;
 pub use lsp::{Position, Url};
 
-use serde::{Deserialize, Serialize};
 use thiserror::Error;
 
 #[derive(Error, Debug)]
@@ -30,19 +29,13 @@ pub mod util {
         let line_start = doc.char_to_utf16_cu(line);
         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.
-#[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),
+        lsp::Position::new(line as u64, col as u64)
+    }
 }
 
 #[derive(Debug, PartialEq, Clone)]
@@ -67,3 +60,5 @@ impl Notification {
         }
     }
 }
+
+pub use jsonrpc::Call;
diff --git a/helix-lsp/src/transport.rs b/helix-lsp/src/transport.rs
index 4ab3d5ec..4c349a13 100644
--- a/helix-lsp/src/transport.rs
+++ b/helix-lsp/src/transport.rs
@@ -2,7 +2,7 @@ use std::collections::HashMap;
 
 use log::debug;
 
-use crate::{Error, Message, Notification};
+use crate::{Error, Notification};
 
 type Result<T> = core::result::Result<T, Error>;
 
@@ -24,10 +24,23 @@ pub(crate) enum Payload {
         value: jsonrpc::MethodCall,
     },
     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 {
-    incoming: Sender<Notification>, // TODO Notification | Call
+    incoming: Sender<jsonrpc::Call>,
     outgoing: Receiver<Payload>,
 
     pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
@@ -42,7 +55,7 @@ impl Transport {
         ex: &Executor,
         reader: BufReader<ChildStdout>,
         writer: BufWriter<ChildStdin>,
-    ) -> (Receiver<Notification>, Sender<Payload>) {
+    ) -> (Receiver<jsonrpc::Call>, Sender<Payload>) {
         let (incoming, rx) = smol::channel::unbounded();
         let (tx, outgoing) = smol::channel::unbounded();
 
@@ -112,6 +125,10 @@ impl Transport {
                 let json = serde_json::to_string(&value)?;
                 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(())
     }
 
-    pub async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> {
+    async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> {
         match msg {
             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) => {
-                debug!("<- {:?}", call);
-                // dispatch
+                self.incoming.send(call).await?;
+                // let notification = Notification::parse(&method, params);
             }
         };
         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 {
             jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
                 debug!("<- {}", result);
@@ -191,6 +202,8 @@ impl Transport {
                     }
                     let msg = msg.unwrap();
 
+                    debug!("<- {:?}", msg);
+
                     self.recv_msg(msg).await.unwrap();
                 }
             }
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index b9594b7e..802dd399 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -433,8 +433,8 @@ impl<'a> Application<'a> {
                 event = reader.next().fuse() => {
                     self.handle_terminal_events(event).await
                 }
-                notification = self.lsp.incoming.next().fuse() => {
-                    self.handle_lsp_notification(notification).await
+                call = self.lsp.incoming.next().fuse() => {
+                    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>) {
-        use helix_lsp::Notification;
-        match notification {
-            Some(Notification::PublishDiagnostics(params)) => {
-                let path = Some(params.uri.to_file_path().unwrap());
-                let view = self
-                    .editor
-                    .views
-                    .iter_mut()
-                    .find(|view| view.doc.path == path);
+    pub async fn handle_lsp_message(&mut self, call: Option<helix_lsp::Call>) {
+        use helix_lsp::{Call, Notification};
+        match call {
+            Some(Call::Notification(helix_lsp::jsonrpc::Notification {
+                method, params, ..
+            })) => {
+                let notification = Notification::parse(&method, params);
+                match notification {
+                    Notification::PublishDiagnostics(params) => {
+                        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 {
-                    let doc = view.doc.text().slice(..);
-                    let diagnostics = params
-                        .diagnostics
-                        .into_iter()
-                        .map(|diagnostic| {
-                            use helix_lsp::util::lsp_pos_to_pos;
-                            let start = lsp_pos_to_pos(&doc, diagnostic.range.start);
-                            let end = lsp_pos_to_pos(&doc, diagnostic.range.end);
+                        if let Some(view) = view {
+                            let doc = view.doc.text().slice(..);
+                            let diagnostics = params
+                                .diagnostics
+                                .into_iter()
+                                .map(|diagnostic| {
+                                    use helix_lsp::util::lsp_pos_to_pos;
+                                    let start = lsp_pos_to_pos(&doc, diagnostic.range.start);
+                                    let end = lsp_pos_to_pos(&doc, diagnostic.range.end);
 
-                            helix_core::Diagnostic {
-                                range: (start, end),
-                                line: diagnostic.range.start.line as usize,
-                                message: diagnostic.message,
-                                // severity
-                                // code
-                                // source
-                            }
-                        })
-                        .collect();
+                                    helix_core::Diagnostic {
+                                        range: (start, end),
+                                        line: diagnostic.range.start.line as usize,
+                                        message: diagnostic.message,
+                                        // severity
+                                        // code
+                                        // source
+                                    }
+                                })
+                                .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!(),
         }
     }
diff --git a/helix-view/src/keymap.rs b/helix-view/src/keymap.rs
index aaba34a6..c815911e 100644
--- a/helix-view/src/keymap.rs
+++ b/helix-view/src/keymap.rs
@@ -82,6 +82,9 @@ use std::collections::HashMap;
 //          = = align?
 //          + =
 //      }
+//
+//      gd = goto definition
+//      gr = goto reference
 // }
 
 #[cfg(feature = "term")]