Repository: clojure-lsp/clojure-lsp
Pull request #336 (2021-02-22 18:19:02)

Add support for incremental text change for faster didChange performance


Patch:
diff --git a/.github/workflows/bump_deps.yml b/.github/workflows/bump_deps.yml
index 60341898..de3b2229 100644
--- a/.github/workflows/bump_deps.yml
+++ b/.github/workflows/bump_deps.yml
@@ -11,6 +11,6 @@ jobs:
     - uses: actions/checkout@v1
 
     - name: Check deps
-      uses: nnichols/clojure-dependency-update-action@v2
+      uses: nnichols/clojure-dependency-update-action@v3
       with:
         github-token: ${{ secrets.github_token }}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d6985ddc..b39a2ecd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,8 +13,6 @@ on:
       - 'mkdocs.yml'
       - '.github/workflows/bump_deps.yml'
   pull_request:
-    branches:
-      - master
     paths-ignore:
       - '**/README.md'
       - '**/CHANGELOG.md'
diff --git a/deps.edn b/deps.edn
index 56773f2a..0686a72d 100644
--- a/deps.edn
+++ b/deps.edn
@@ -1,7 +1,7 @@
 {:deps {org.clojure/clojure {:mvn/version "1.10.2"}
         org.clojure/tools.reader {:mvn/version "1.3.5"}
         org.eclipse.lsp4j/org.eclipse.lsp4j {:mvn/version "0.10.0"  :exclusions [org.eclipse.xtend/org.eclipse.xtend.lib]}
-        org.eclipse.xtend/org.eclipse.xtend.lib {:mvn/version "2.25.0.M2" :exclusions [com.google.guava/guava]}
+        org.eclipse.xtend/org.eclipse.xtend.lib {:mvn/version "2.25.0.M3" :exclusions [com.google.guava/guava]}
         com.google.guava/guava {:mvn/version "30.1-jre"}
         rewrite-clj/rewrite-clj {:mvn/version "0.6.1"}
         org.clojure/data.json {:mvn/version "1.0.0"}
diff --git a/src/clojure_lsp/config.clj b/src/clojure_lsp/config.clj
index 3d8f88b5..0d5dab8b 100644
--- a/src/clojure_lsp/config.clj
+++ b/src/clojure_lsp/config.clj
@@ -3,6 +3,8 @@
     [clojure-lsp.shared :as shared]
     [clojure.string :as string]))
 
+(def change-debounce-ms 300)
+
 (defn kondo-for-paths [paths]
   {:cache true
    :parallel true
diff --git a/src/clojure_lsp/db.clj b/src/clojure_lsp/db.clj
index 53bcc216..4ede497c 100644
--- a/src/clojure_lsp/db.clj
+++ b/src/clojure_lsp/db.clj
@@ -8,6 +8,7 @@
    [taoensso.timbre :as log]))
 
 (defonce db (atom {:documents {}}))
+(defonce current-changes-chan (async/chan 1))
 (defonce diagnostics-chan (async/chan 1))
 (defonce edits-chan (async/chan 1))
 
diff --git a/src/clojure_lsp/feature/file_management.clj b/src/clojure_lsp/feature/file_management.clj
index 93c97e57..4f23727d 100644
--- a/src/clojure_lsp/feature/file_management.clj
+++ b/src/clojure_lsp/feature/file_management.clj
@@ -73,16 +73,41 @@
             (f.diagnostic/notify uri new-analysis))))
       references-uri)))
 
-(defn did-change [uri text version]
-  ;; Ensure we are only accepting newer changes
+(defn ^:private offsets [lines line col end-line end-col]
+  (loop [lines (seq lines)
+         offset 0
+         idx 0]
+    (if (or (not lines)
+            (= line idx))
+      [(+ offset col line)
+       (loop [lines lines
+              offset offset
+              idx idx]
+         (if (or (not lines)
+                 (= end-line idx))
+           (+ offset end-col end-line)
+           (recur (next lines)
+                  (+ offset (count (first lines)))
+                  (inc idx))))]
+      (recur (next lines)
+             (+ offset (count (first lines)))
+             (inc idx)))))
+
+(defn ^:private replace-text [original replacement line col end-line end-col]
+  (let [lines (string/split original #"\n") ;; don't use OS specific line delimiter!
+        [start-offset end-offset] (offsets lines line col end-line end-col)
+        [prefix suffix] [(subs original 0 start-offset)
+                         (subs original end-offset (count original))]]
+    (string/join [prefix replacement suffix])))
+
+(defn analyze-changes [{:keys [uri text version]}]
   (let [notify-references? (get-in @db/db [:settings :notify-references-on-file-change] false)]
     (loop [state-db @db/db]
-      (when (> version (get-in state-db [:documents uri :v] -1))
+      (when (>= version (get-in state-db [:documents uri :v] -1))
         (when-let [new-analysis (crawler/run-kondo-on-text! text uri)]
           (let [filename (shared/uri->filename uri)
                 old-analysis (get-in @db/db [:analysis filename])]
             (if (compare-and-set! db/db state-db (-> state-db
-                                                     (assoc-in [:documents uri] {:v version :text text})
                                                      (crawler/update-analysis uri (:analysis new-analysis))
                                                      (crawler/update-findings uri (:findings new-analysis))))
               (do
@@ -91,6 +116,23 @@
                   (notify-references old-analysis (get-in @db/db [:analysis filename]))))
               (recur @db/db))))))))
 
+(defn did-change [uri changes version]
+  (loop [state-db @db/db]
+    (let [old-text (get-in @db/db [:documents uri :text])
+          final-text (reduce (fn [old-text {new-text :text {{start-line :line
+                                                             start-character :character} :start
+                                                            {end-line :line
+                                                             end-character :character} :end} :range}]
+                               (replace-text old-text new-text start-line start-character end-line end-character)) old-text changes)]
+      (if (compare-and-set! db/db state-db (-> state-db
+                                               (assoc-in [:documents uri :v] version)
+                                               (assoc-in [:documents uri :text] final-text)))
+
+        (async/put! db/current-changes-chan {:uri uri
+                                             :text final-text
+                                             :version version})
+        (recur @db/db)))))
+
 (defn force-get-document-text
   "Get document text from db, if document not found, tries to open the document"
   [uri]
diff --git a/src/clojure_lsp/handlers.clj b/src/clojure_lsp/handlers.clj
index 225b05e9..36059fb9 100644
--- a/src/clojure_lsp/handlers.clj
+++ b/src/clojure_lsp/handlers.clj
@@ -53,8 +53,8 @@
   (when (get-in @db/db [:documents textDocument :saved-on-disk])
     (swap! db/db #(update % :documents dissoc textDocument))))
 
-(defn did-change [uri text version]
-  (f.file-management/did-change uri text version))
+(defn did-change [{:keys [textDocument contentChanges]}]
+  (f.file-management/did-change (:uri textDocument) contentChanges (:version textDocument)))
 
 (defn did-change-watched-files [changes]
   (let [uris (map :uri (filter (comp #{:deleted} :type) changes))]
diff --git a/src/clojure_lsp/interop.clj b/src/clojure_lsp/interop.clj
index a49f7e40..fbf80e75 100644
--- a/src/clojure_lsp/interop.clj
+++ b/src/clojure_lsp/interop.clj
@@ -74,6 +74,10 @@
 (defmethod j/from-java TextDocumentIdentifier [^TextDocumentIdentifier instance]
   (document->decoded-uri instance))
 
+(defmethod j/from-java VersionedTextDocumentIdentifier [^VersionedTextDocumentIdentifier instance]
+  {:version (.getVersion instance)
+   :uri (document->decoded-uri instance)})
+
 (defmethod j/from-java JsonElement [^JsonElement instance]
   (-> instance
       .toString
diff --git a/src/clojure_lsp/main.clj b/src/clojure_lsp/main.clj
index b5bd81df..41afd2f5 100644
--- a/src/clojure_lsp/main.clj
+++ b/src/clojure_lsp/main.clj
@@ -1,74 +1,75 @@
 (ns clojure-lsp.main
   (:require
-    [clojure-lsp.db :as db]
-    [clojure-lsp.feature.refactor :as f.refactor]
-    [clojure-lsp.feature.semantic-tokens :as semantic-tokens]
-    [clojure-lsp.handlers :as handlers]
-    [clojure-lsp.interop :as interop]
-    [clojure-lsp.nrepl :as nrepl]
-    [clojure-lsp.producer :as producer]
-    [clojure-lsp.shared :as shared]
-    [clojure.core.async :as async]
-    [taoensso.timbre :as log]
-    borkdude.dynaload)
+   [clojure-lsp.config :as config]
+   [clojure-lsp.db :as db]
+   [clojure-lsp.feature.file-management :as f.file-management]
+   [clojure-lsp.feature.refactor :as f.refactor]
+   [clojure-lsp.feature.semantic-tokens :as semantic-tokens]
+   [clojure-lsp.handlers :as handlers]
+   [clojure-lsp.interop :as interop]
+   [clojure-lsp.nrepl :as nrepl]
+   [clojure-lsp.producer :as producer]
+   [clojure-lsp.shared :as shared]
+   [clojure.core.async :refer [<! go-loop timeout thread]]
+   [taoensso.timbre :as log]
+   borkdude.dynaload)
   (:import
-    (clojure_lsp ClojureExtensions)
-    (java.util.concurrent CompletableFuture)
-    (java.util.function Supplier)
-    (org.eclipse.lsp4j
-      CallHierarchyIncomingCallsParams
-      CallHierarchyPrepareParams
-      CodeActionParams
-      CodeAction
-      CodeActionOptions
-      CodeLens
-      CodeLensParams
-      CodeLensOptions
-      CompletionItem
-      CompletionOptions
-      CompletionParams
-      DefinitionParams
-      DidChangeConfigurationParams
-      DidChangeTextDocumentParams
-      DidChangeWatchedFilesParams
-      DidChangeWatchedFilesRegistrationOptions
-      DidCloseTextDocumentParams
-      DidOpenTextDocumentParams
-      DidSaveTextDocumentParams
-      DocumentFormattingParams
-      DocumentHighlightParams
-      DocumentRangeFormattingParams
-      DocumentSymbolParams
-      ExecuteCommandOptions
-      ExecuteCommandParams
-      FileSystemWatcher
-      HoverParams
-      InitializeParams
-      InitializeResult
-      InitializedParams
-      ReferenceParams
-      Registration
-      RegistrationParams
-      RenameParams
-      SaveOptions
-      SemanticTokensLegend
-      SemanticTokensParams
-      SemanticTokensRangeParams
-      SemanticTokensWithRegistrationOptions
-      ServerCapabilities
-      SignatureHelpOptions
-      SignatureHelpParams
-      TextDocumentContentChangeEvent
-      TextDocumentSyncKind
-      TextDocumentSyncOptions
-      WorkspaceSymbolParams)
-    (org.eclipse.lsp4j.launch LSPLauncher)
-    (org.eclipse.lsp4j.services LanguageServer TextDocumentService WorkspaceService LanguageClient))
+   (clojure_lsp ClojureExtensions)
+   (java.util.concurrent CompletableFuture)
+   (java.util.function Supplier)
+   (org.eclipse.lsp4j
+    CallHierarchyIncomingCallsParams
+    CallHierarchyPrepareParams
+    CodeActionParams
+    CodeAction
+    CodeActionOptions
+    CodeLens
+    CodeLensParams
+    CodeLensOptions
+    CompletionItem
+    CompletionOptions
+    CompletionParams
+    DefinitionParams
+    DidChangeConfigurationParams
+    DidChangeTextDocumentParams
+    DidChangeWatchedFilesParams
+    DidChangeWatchedFilesRegistrationOptions
+    DidCloseTextDocumentParams
+    DidOpenTextDocumentParams
+    DidSaveTextDocumentParams
+    DocumentFormattingParams
+    DocumentHighlightParams
+    DocumentRangeFormattingParams
+    DocumentSymbolParams
+    ExecuteCommandOptions
+    ExecuteCommandParams
+    FileSystemWatcher
+    HoverParams
+    InitializeParams
+    InitializeResult
+    InitializedParams
+    ReferenceParams
+    Registration
+    RegistrationParams
+    RenameParams
+    SaveOptions
+    SemanticTokensLegend
+    SemanticTokensParams
+    SemanticTokensRangeParams
+    SemanticTokensWithRegistrationOptions
+    ServerCapabilities
+    SignatureHelpOptions
+    SignatureHelpParams
+    TextDocumentSyncKind
+    TextDocumentSyncOptions
+    WorkspaceSymbolParams)
+   (org.eclipse.lsp4j.launch LSPLauncher)
+   (org.eclipse.lsp4j.services LanguageServer TextDocumentService WorkspaceService LanguageClient))
   (:gen-class))
 
 (defonce formatting (atom false))
 
-(defmacro go [id & body]
+(defmacro start [id & body]
   `(let [~'_start-time (System/nanoTime)
          ~'_id ~id]
      (do ~@body)))
@@ -88,168 +89,164 @@
 (defmacro ^:private sync-handler
   ([params handler]
    `(end
-      (->> ~params
-           interop/java->clj
-           ~handler)))
+     (->> ~params
+          interop/java->clj
+          ~handler)))
   ([params handler response-spec]
    `(end
-      (->> ~params
-           interop/java->clj
-           ~handler
-           (interop/conform-or-log ~response-spec)))))
+     (->> ~params
+          interop/java->clj
+          ~handler
+          (interop/conform-or-log ~response-spec)))))
 
 (defmacro ^:private async-handler
   [params handler response-spec]
   `(CompletableFuture/supplyAsync
-     (reify Supplier
-       (get [this]
-         (sync-handler ~params ~handler ~response-spec)))))
+    (reify Supplier
+      (get [this]
+        (sync-handler ~params ~handler ~response-spec)))))
 
 (deftype LSPTextDocumentService []
   TextDocumentService
   (^void didOpen [_ ^DidOpenTextDocumentParams params]
-    (go :didOpen
-        (sync-handler params handlers/did-open)))
+    (start :didOpen
+           (sync-handler params handlers/did-open)))
 
   (^void didChange [_ ^DidChangeTextDocumentParams params]
-    (go :didChange
-        (end
-          (let [textDocument (.getTextDocument params)
-                version (.getVersion textDocument)
-                changes (.getContentChanges params)
-                text (.getText ^TextDocumentContentChangeEvent (.get changes 0))
-                uri (interop/document->decoded-uri textDocument)]
-            (handlers/did-change uri text version)))))
+    (start :didChange
+           (sync-handler params handlers/did-change)))
 
   (^void didSave [_ ^DidSaveTextDocumentParams params]
-    (go :didSave
-        (sync-handler params handlers/did-save)))
+    (start :didSave
+           (future
+             (sync-handler params handlers/did-save)))
+    (CompletableFuture/completedFuture 0))
 
   (^void didClose [_ ^DidCloseTextDocumentParams _params]
-    (go :didClose
-        nil))
+    (start :didClose
+           nil))
 
   (^CompletableFuture references [_ ^ReferenceParams params]
-    (go :references
-        (async-handler params handlers/references ::interop/references)))
+    (start :references
+           (async-handler params handlers/references ::interop/references)))
 
   (^CompletableFuture completion [_ ^CompletionParams params]
-    (go :completion
-        (async-handler params handlers/completion ::interop/completion-items)))
+    (start :completion
+           (async-handler params handlers/completion ::interop/completion-items)))
 
-(^CompletableFuture resolveCompletionItem [_ ^CompletionItem item]
-    (go :resolveCompletionItem
-        (async-handler item handlers/completion-resolve-item ::interop/completion-item)))
+  (^CompletableFuture resolveCompletionItem [_ ^CompletionItem item]
+    (start :resolveCompletionItem
+           (async-handler item handlers/completion-resolve-item ::interop/completion-item)))
 
   (^CompletableFuture rename [_ ^RenameParams params]
-    (go :rename
-        (async-handler params handlers/rename ::interop/workspace-edit)))
+    (start :rename
+           (async-handler params handlers/rename ::interop/workspace-edit)))
 
   (^CompletableFuture hover [_ ^HoverParams params]
-    (go :hover
-        (async-handler params handlers/hover ::interop/hover)))
+    (start :hover
+           (async-handler params handlers/hover ::interop/hover)))
 
   (^CompletableFuture signatureHelp [_ ^SignatureHelpParams params]
-    (go :signatureHelp
-        (async-handler params handlers/signature-help ::interop/signature-help)))
+    (start :signatureHelp
+           (async-handler params handlers/signature-help ::interop/signature-help)))
 
   (^CompletableFuture formatting [_ ^DocumentFormattingParams params]
-    (go :formatting
-        (async-handler params handlers/formatting ::interop/edits)))
+    (start :formatting
+           (async-handler params handlers/formatting ::interop/edits)))
 
   (^CompletableFuture rangeFormatting [_this ^DocumentRangeFormattingParams params]
-    (go :rangeFormatting
-        (end
-         (let [result (when (compare-and-set! formatting false true)
-                        (try
-                          (let [doc-id (interop/document->decoded-uri (.getTextDocument params))
-                                range (.getRange params)
-                                start (.getStart range)
-                                end (.getEnd range)]
-                            (interop/conform-or-log ::interop/edits (#'handlers/range-formatting
-                                                                     doc-id
-                                                                     {:row (inc (.getLine start))
-                                                                      :col (inc (.getCharacter start))
-                                                                      :end-row (inc (.getLine end))
-                                                                      :end-col (inc (.getCharacter end))})))
-                          (catch Exception e
-                            (log/error e))
-                          (finally
-                            (reset! formatting false))))]
-           (CompletableFuture/completedFuture
-            result)))))
+    (start :rangeFormatting
+           (end
+            (let [result (when (compare-and-set! formatting false true)
+                           (try
+                             (let [doc-id (interop/document->decoded-uri (.getTextDocument params))
+                                   range (.getRange params)
+                                   start (.getStart range)
+                                   end (.getEnd range)]
+                               (interop/conform-or-log ::interop/edits (#'handlers/range-formatting
+                                                                        doc-id
+                                                                        {:row (inc (.getLine start))
+                                                                         :col (inc (.getCharacter start))
+                                                                         :end-row (inc (.getLine end))
+                                                                         :end-col (inc (.getCharacter end))})))
+                             (catch Exception e
+                               (log/error e))
+                             (finally
+                               (reset! formatting false))))]
+              (CompletableFuture/completedFuture
+               result)))))
 
   (^CompletableFuture codeAction [_ ^CodeActionParams params]
-    (go :codeAction
-        (async-handler params handlers/code-actions ::interop/code-actions)))
+    (start :codeAction
+           (async-handler params handlers/code-actions ::interop/code-actions)))
 
   (^CompletableFuture resolveCodeAction [_ ^CodeAction unresolved]
-    (go :resolveCodeAction
-        (async-handler unresolved handlers/resolve-code-action ::interop/code-action)))
+    (start :resolveCodeAction
+           (async-handler unresolved handlers/resolve-code-action ::interop/code-action)))
 
   (^CompletableFuture codeLens [_ ^CodeLensParams params]
-    (go :codeLens
-        (async-handler params handlers/code-lens ::interop/code-lenses)))
+    (start :codeLens
+           (async-handler params handlers/code-lens ::interop/code-lenses)))
 
   (^CompletableFuture resolveCodeLens [_ ^CodeLens params]
-    (go :resolveCodeLens
-        (async-handler params handlers/code-lens-resolve ::interop/code-lens)))
+    (start :resolveCodeLens
+           (async-handler params handlers/code-lens-resolve ::interop/code-lens)))
 
   (^CompletableFuture definition [_ ^DefinitionParams params]
-    (go :definition
-        (async-handler params handlers/definition ::interop/location)))
+    (start :definition
+           (async-handler params handlers/definition ::interop/location)))
 
   (^CompletableFuture documentSymbol [_ ^DocumentSymbolParams params]
-    (go :documentSymbol
-        (async-handler params handlers/document-symbol ::interop/document-symbols)))
+    (start :documentSymbol
+           (async-handler params handlers/document-symbol ::interop/document-symbols)))
 
   (^CompletableFuture documentHighlight [_ ^DocumentHighlightParams params]
-    (go :documentHighlight
-        (async-handler params handlers/document-highlight ::interop/document-highlights)))
+    (start :documentHighlight
+           (async-handler params handlers/document-highlight ::interop/document-highlights)))
 
   (^CompletableFuture semanticTokensFull [_ ^SemanticTokensParams params]
-    (go :semanticTokensFull
-        (async-handler params handlers/semantic-tokens-full ::interop/semantic-tokens)))
+    (start :semanticTokensFull
+           (async-handler params handlers/semantic-tokens-full ::interop/semantic-tokens)))
 
   (^CompletableFuture semanticTokensRange [_ ^SemanticTokensRangeParams params]
-    (go :semanticTokensRange
-        (async-handler params handlers/semantic-tokens-range ::interop/semantic-tokens)))
+    (start :semanticTokensRange
+           (async-handler params handlers/semantic-tokens-range ::interop/semantic-tokens)))
 
   (^CompletableFuture prepareCallHierarchy [_ ^CallHierarchyPrepareParams params]
-    (go :prepareCallHierarchy
-        (async-handler params handlers/prepare-call-hierarchy ::interop/call-hierarchy-items)))
+    (start :prepareCallHierarchy
+           (async-handler params handlers/prepare-call-hierarchy ::interop/call-hierarchy-items)))
 
   (^CompletableFuture callHierarchyIncomingCalls [_ ^CallHierarchyIncomingCallsParams params]
-    (go :callHierarchyIncomingCalls
-        (async-handler params handlers/call-hierarchy-incoming ::interop/call-hierarchy-incoming-calls))))
+    (start :callHierarchyIncomingCalls
+           (async-handler params handlers/call-hierarchy-incoming ::interop/call-hierarchy-incoming-calls))))
 
 (deftype LSPWorkspaceService []
   WorkspaceService
   (^CompletableFuture executeCommand [_ ^ExecuteCommandParams params]
-    (go :executeCommand
-        (future
-          (sync-handler params handlers/execute-command)))
+    (start :executeCommand
+           (future
+             (sync-handler params handlers/execute-command)))
     (CompletableFuture/completedFuture 0))
 
   (^void didChangeConfiguration [_ ^DidChangeConfigurationParams params]
     (log/warn (interop/java->clj params)))
 
   (^void didChangeWatchedFiles [_ ^DidChangeWatchedFilesParams params]
-    (go :didChangeWatchedFiles
-        (end
-          (some->> params
-                   (.getChanges)
-                   (interop/conform-or-log ::interop/watched-files-changes)
-                   (handlers/did-change-watched-files)))))
+    (start :didChangeWatchedFiles
+           (end
+            (some->> params
+                     (.getChanges)
+                     (interop/conform-or-log ::interop/watched-files-changes)
+                     (handlers/did-change-watched-files)))))
 
   ;; TODO wait for lsp4j release
   #_(^void didDeleteFiles [_ ^DeleteFilesParams params]
-                          (go :didSave
-                              (sync-handler params handlers/did-delete-files)))
+                          (start :didSave
+                                 (sync-handler params handlers/did-delete-files)))
 
   (^CompletableFuture symbol [_ ^WorkspaceSymbolParams params]
-    (go :workspaceSymbol
-        (async-handler params handlers/workspace-symbols ::interop/workspace-symbols))))
+    (start :workspaceSymbol
+           (async-handler params handlers/workspace-symbols ::interop/workspace-symbols))))
 
 (defn ^:private client-settings [^InitializeParams params]
   (-> params
@@ -266,15 +263,15 @@
 
 ;; Called from java
 (defn extension [method & args]
-  (go :extension
-      (CompletableFuture/completedFuture
-        (end
-          (apply #'handlers/extension method args)))))
+  (start :extension
+         (CompletableFuture/completedFuture
+          (end
+           (apply #'handlers/extension method args)))))
 
 (defn ^:private start-parent-process-liveness-probe!
   [ppid server]
-  (async/go-loop []
-    (async/<! (async/timeout 5000))
+  (go-loop []
+    (<! (timeout 5000))
     (if (shared/process-alive? ppid)
       (recur)
       (do
@@ -284,62 +281,62 @@
 (def server
   (proxy [ClojureExtensions LanguageServer] []
     (^CompletableFuture initialize [^InitializeParams params]
-      (go :initialize
-          (end
-            (do
-              (log/info "Initializing...")
-              (#'handlers/initialize (.getRootUri params)
-                                     (client-capabilities params)
-                                     (client-settings params))
-              (when-let [parent-process-id (.getProcessId params)]
-                (start-parent-process-liveness-probe! parent-process-id this))
-              (let [settings (:settings @db/db)]
-                (CompletableFuture/completedFuture
-                  (InitializeResult. (doto (ServerCapabilities.)
-                                       (.setDocumentHighlightProvider true)
-                                       (.setHoverProvider true)
-                                       (.setSignatureHelpProvider (SignatureHelpOptions. []))
-                                       (.setCallHierarchyProvider true)
-                                       (.setCodeActionProvider (doto (CodeActionOptions. interop/code-action-kind)
-                                                                 (.setResolveProvider true)))
-                                       (.setCodeLensProvider (CodeLensOptions. true))
-                                       (.setReferencesProvider true)
-                                       (.setRenameProvider true)
-                                       (.setDefinitionProvider true)
-                                       (.setDocumentFormattingProvider (:document-formatting? settings))
-                                       (.setDocumentRangeFormattingProvider (:document-range-formatting? settings))
-                                       (.setDocumentSymbolProvider true)
-                                       (.setWorkspaceSymbolProvider true)
-                                       (.setSemanticTokensProvider (when (or (not (contains? settings :semantic-tokens?))
-                                                                             (:semantic-tokens? settings))
-                                                                     (doto (SemanticTokensWithRegistrationOptions.)
-                                                                       (.setLegend (doto (SemanticTokensLegend.
+      (start :initialize
+             (end
+              (do
+                (log/info "Initializing...")
+                (#'handlers/initialize (.getRootUri params)
+                                       (client-capabilities params)
+                                       (client-settings params))
+                (when-let [parent-process-id (.getProcessId params)]
+                  (start-parent-process-liveness-probe! parent-process-id this))
+                (let [settings (:settings @db/db)]
+                  (CompletableFuture/completedFuture
+                   (InitializeResult. (doto (ServerCapabilities.)
+                                        (.setDocumentHighlightProvider true)
+                                        (.setHoverProvider true)
+                                        (.setSignatureHelpProvider (SignatureHelpOptions. []))
+                                        (.setCallHierarchyProvider true)
+                                        (.setCodeActionProvider (doto (CodeActionOptions. interop/code-action-kind)
+                                                                  (.setResolveProvider true)))
+                                        (.setCodeLensProvider (CodeLensOptions. true))
+                                        (.setReferencesProvider true)
+                                        (.setRenameProvider true)
+                                        (.setDefinitionProvider true)
+                                        (.setDocumentFormattingProvider (:document-formatting? settings))
+                                        (.setDocumentRangeFormattingProvider (:document-range-formatting? settings))
+                                        (.setDocumentSymbolProvider true)
+                                        (.setWorkspaceSymbolProvider true)
+                                        (.setSemanticTokensProvider (when (or (not (contains? settings :semantic-tokens?))
+                                                                              (:semantic-tokens? settings))
+                                                                      (doto (SemanticTokensWithRegistrationOptions.)
+                                                                        (.setLegend (doto (SemanticTokensLegend.
                                                                                            semantic-tokens/token-types-str
                                                                                            semantic-tokens/token-modifiers)))
-                                                                       (.setRange true)
-                                                                       (.setFull true))))
-                                       (.setExecuteCommandProvider (doto (ExecuteCommandOptions.)
-                                                                     (.setCommands f.refactor/available-refactors)))
-                                       (.setTextDocumentSync (doto (TextDocumentSyncOptions.)
-                                                               (.setOpenClose true)
-                                                               (.setChange TextDocumentSyncKind/Full)
-                                                               (.setSave (SaveOptions. true))))
-                                       (.setCompletionProvider (CompletionOptions. true []))))))))))
+                                                                        (.setRange true)
+                                                                        (.setFull true))))
+                                        (.setExecuteCommandProvider (doto (ExecuteCommandOptions.)
+                                                                      (.setCommands f.refactor/available-refactors)))
+                                        (.setTextDocumentSync (doto (TextDocumentSyncOptions.)
+                                                                (.setOpenClose true)
+                                                                (.setChange TextDocumentSyncKind/Incremental)
+                                                                (.setSave (SaveOptions. true))))
+                                        (.setCompletionProvider (CompletionOptions. true []))))))))))
 
     (^void initialized [^InitializedParams params]
-      (go :initialized
-          (end
-            (do
-              (log/info "Initialized!")
-              (let [client ^LanguageClient (:client @db/db)]
-                (.registerCapability client
-                                     (RegistrationParams. [(Registration. "id" "workspace/didChangeWatchedFiles"
-                                                                          (DidChangeWatchedFilesRegistrationOptions. [(FileSystemWatcher. "**")]))])))))))
+      (start :initialized
+             (end
+              (do
+                (log/info "Initialized!")
+                (let [client ^LanguageClient (:client @db/db)]
+                  (.registerCapability client
+                                       (RegistrationParams. [(Registration. "id" "workspace/didChangeWatchedFiles"
+                                                                            (DidChangeWatchedFilesRegistrationOptions. [(FileSystemWatcher. "**")]))])))))))
     (^CompletableFuture shutdown []
       (log/info "Shutting down")
       (reset! db/db {:documents {}}) ;; TODO confirm this is correct
       (CompletableFuture/completedFuture
-        {:result nil}))
+       {:result nil}))
     (exit []
       (log/info "Exit")
       (shutdown-agents)
@@ -353,7 +350,7 @@
   (let [buffer-size 1024
         os (java.io.PipedOutputStream.)
         is (java.io.PipedInputStream. os)]
-    (async/thread
+    (thread
       (try
         (let [buffer (byte-array buffer-size)]
           (loop [chs (.read system-in buffer 0 buffer-size)]
@@ -369,7 +366,7 @@
   (let [buffer-size 1024
         is (java.io.PipedInputStream.)
         os (java.io.PipedOutputStream. is)]
-    (async/thread
+    (thread
       (try
         (let [buffer (byte-array buffer-size)]
           (loop [chs (.read is buffer 0 buffer-size)]
@@ -388,14 +385,16 @@
         launcher (LSPLauncher/createServerLauncher server is os)]
     (nrepl/setup-nrepl)
     (swap! db/db assoc :client ^LanguageClient (.getRemoteProxy launcher))
-    (async/go
-      (loop [edit (async/<! db/edits-chan)]
-        (producer/workspace-apply-edit edit)
-        (recur (async/<! db/edits-chan))))
-    (async/go
-      (loop [diagnostic (async/<! db/diagnostics-chan)]
-        (producer/publish-diagnostic diagnostic)
-        (recur (async/<! db/diagnostics-chan))))
+    (go-loop [edit (<! db/edits-chan)]
+      (producer/workspace-apply-edit edit)
+      (recur (<! db/edits-chan)))
+    (go-loop [diagnostic (<! db/diagnostics-chan)]
+      (producer/publish-diagnostic diagnostic)
+      (recur (<! db/diagnostics-chan)))
+    (let [debounced-chan (shared/debounce db/current-changes-chan config/change-debounce-ms)]
+      (go-loop []
+        (f.file-management/analyze-changes (<! debounced-chan))
+        (recur)))
     (.startListening launcher)))
 
 (defn ^:private setup-logging []
diff --git a/src/clojure_lsp/parser.clj b/src/clojure_lsp/parser.clj
index 58497a8c..2fae4ae6 100644
--- a/src/clojure_lsp/parser.clj
+++ b/src/clojure_lsp/parser.clj
@@ -94,8 +94,6 @@
       safe-zloc-of-string
       (find-last-by-pos {:row row :col col :end-row row :end-col col})))
 
-(loc-at-pos "1\n#?(:clj \"clojure\" :cljs \"clojurescript\")\n2" 1 1)
-
 (defn safe-loc-at-pos [text row col]
   (try
     (loc-at-pos text row (dec col))
diff --git a/src/clojure_lsp/shared.clj b/src/clojure_lsp/shared.clj
index 13d35806..b51ef89b 100644
--- a/src/clojure_lsp/shared.clj
+++ b/src/clojure_lsp/shared.clj
@@ -1,6 +1,7 @@
 (ns clojure-lsp.shared
   (:require
     [clojure-lsp.db :as db]
+    [clojure.core.async :refer [<! >! put! take! go alts! chan go-loop timeout close! sliding-buffer]]
     [clojure.java.io :as io]
     [clojure.java.shell :as shell]
     [clojure.string :as string]
@@ -116,3 +117,14 @@
 (defn position->line-column [position]
   [(inc (:line position))
    (inc (:character position))])
+
+(defn debounce [in ms]
+  (let [out (chan)]
+    (go-loop [last-val nil]
+      (let [val (if (nil? last-val) (<! in) last-val)
+            timer (timeout ms)
+            [new-val ch] (alts! [in timer])]
+        (condp = ch
+          timer (do (>! out val) (recur nil))
+          in (recur new-val))))
+    out))


Test patch:
diff --git a/test/clojure_lsp/features/file_management_test.clj b/test/clojure_lsp/features/file_management_test.clj
index da020054..68e99282 100644
--- a/test/clojure_lsp/features/file_management_test.clj
+++ b/test/clojure_lsp/features/file_management_test.clj
@@ -7,6 +7,16 @@
 
 (h/reset-db-after-test)
 
+(deftest update-text
+  (is (= "(comment\n   )" (#'f.file-management/replace-text "(comment)" "\n   " 0 8 0 8)))
+  (is (= "some \nboring\n text" (#'f.file-management/replace-text "some \ncool\n text" "boring" 1 0 1 4)))
+  (is (= "(+ 1 2)" (#'f.file-management/replace-text "(+ 1 1)" "2" 0 5 0 6)))
+  (is (= "(+ 1)" (#'f.file-management/replace-text "(+ 1 1)" "" 0 4 0 6)))
+  (is (= "\n\n (+ 1 2)\n" (#'f.file-management/replace-text "\n\n (let [a 1\n   b 2]\n   (+ 1 2))\n" "(+ 1 2)" 2 1 4 11)))
+  (is (= "\r\n\r\n (+ 1 2)\r\n" (#'f.file-management/replace-text "\r\n\r\n (let [a 1\r\n   b 2]\r\n   (+ 1 2))\r\n" "(+ 1 2)" 2 1 4 11)))
+  (is (= "\n\n (let [a 1\n   b 2]\n   (+ 1 2))\n" (#'f.file-management/replace-text "\n\n (+ 1 2)\n" "(let [a 1\n   b 2]\n   (+ 1 2))" 2 1 2 8)))
+  (is (= "(+ 1 1)\n\n" (#'f.file-management/replace-text "(+ 1 1)\n" "\n" 1 0 1 0))))
+
 (deftest uri->namespace
   (testing "when don't have a project root"
     (reset! db/db {})