<commit_before>(ns nightcode.editors
  (:require [clojure.java.io :as io]
            [compliment.core :as compliment]
            [flatland.ordered.map :as flatland]
            [nightcode.file-browser :as file-browser]
            [nightcode.shortcuts :as shortcuts]
            [nightcode.ui :as ui]
            [nightcode.utils :as utils]
            [paredit.loc-utils :as loc-utils]
            [paredit.static-analysis :as static-analysis]
            [paredit.parser :as parser]
            [paredit-widget.core :as pw]
            [seesaw.color :as color]
            [seesaw.core :as s])
  (:import [com.camick TextPrompt]
           [java.awt.event KeyEvent KeyListener]
           [javax.swing JComponent KeyStroke]
           [javax.swing.event DocumentListener HyperlinkEvent$EventType]
           [nightcode.ui JConsole]
           [org.fife.ui.autocomplete
            AutoCompletion BasicCompletion DefaultCompletionProvider]
           [org.fife.ui.rsyntaxtextarea FileLocation TextEditorPane Theme]
           [org.fife.ui.rtextarea RTextScrollPane SearchContext SearchEngine]))

(def editors (atom (flatland/ordered-map)))
(def font-size (atom (utils/read-pref :font-size)))
(def paredit-enabled? (atom (or (utils/read-pref :enable-paredit) false)))
(def doc-enabled? (atom (or (utils/read-pref :enable-doc) false)))
(def tabs (atom nil))
(def theme-resource (atom (io/resource "dark.xml")))

; basic getters

(defn get-text-area
  [view]
  (when view
    (->> [:<org.fife.ui.rsyntaxtextarea.TextEditorPane>]
         (s/select view)
         first)))

(defn get-text-area-from-path
  [path]
  (get-text-area (get-in @editors [path :view])))

(defn get-selected-text-area
  []
  (get-text-area-from-path @ui/tree-selection))

(defn get-selected-editor
  []
  (get-in @editors [@ui/tree-selection :view]))

(defn unsaved?
  [path]
  (when-let [text-area (get-text-area-from-path path)]
    (.isDirty text-area)))

(defn get-editor-text
  []
  (when-let [text-area (get-selected-text-area)]
    (.getText text-area)))

(defn get-editor-selected-text
  []
  (when-let [text-area (get-selected-text-area)]
    (.getSelectedText text-area)))

; tabs

(def ^:dynamic *reorder-tabs?* true)

(defn move-tab-selection!
  [diff]
  (let [paths (reverse (keys @editors))
        index (.indexOf paths @ui/tree-selection)
        max-index (- (count paths) 1)
        new-index (+ index diff)
        new-index (cond
                    (< new-index 0) max-index
                    (> new-index max-index) 0
                    :else new-index)]
    (when (> (count paths) 0)
      (binding [*reorder-tabs?* false]
        (ui/update-project-tree! (nth paths new-index)))))
  true)

(defn update-tabs!
  [path]
  (doto @ui/root .invalidate .validate)
  (let [editor-pane (ui/get-editor-pane)]
    (when @tabs (.closeBalloon @tabs))
    (->> (for [[e-path {:keys [italicize-fn]}] (reverse @editors)]
           (format "<a href='%s' style='text-decoration: %s;
                                        font-style: %s;'>%s</a>"
                   e-path
                   (if (utils/parent-path? path e-path) "underline" "none")
                   (if (italicize-fn) "italic" "normal")
                   (-> e-path io/file .getName)))
         (cons "<center>PgUp PgDn</center>")
         (clojure.string/join "<br/>")
         shortcuts/wrap-hint-text
         (s/editor-pane :editable? false :content-type "text/html" :text)
         (shortcuts/create-hint! true editor-pane)
         (reset! tabs))
    (s/listen (.getContents @tabs)
              :hyperlink
              (fn [e]
                (when (= (.getEventType e) HyperlinkEvent$EventType/ACTIVATED)
                  (binding [*reorder-tabs?* false]
                    (ui/update-project-tree! (.getDescription e))))))
    (shortcuts/toggle-hint! @tabs @shortcuts/down?)))

; button bar actions

(defn update-buttons!
  [editor ^TextEditorPane text-area]
  (when (ui/config! editor :#save :enabled? (.isDirty text-area))
    (update-tabs! @ui/tree-selection))
  (ui/config! editor :#undo :enabled? (.canUndo text-area))
  (ui/config! editor :#redo :enabled? (.canRedo text-area)))

(defn save-file!
  [& _]
  (when-let [text-area (get-selected-text-area)]
    (io!
      (with-open [w (io/writer (io/file @ui/tree-selection))]
        (.write text-area w)))
    (.setDirty text-area false)
    (s/request-focus! text-area)
    (update-buttons! (get-selected-editor) text-area))
  true)

(defn undo-file!
  [& _]
  (when-let [text-area (get-selected-text-area)]
    (.undoLastAction text-area)
    (update-buttons! (get-selected-editor) text-area)))

(defn redo-file!
  [& _]
  (when-let [text-area (get-selected-text-area)]
    (.redoLastAction text-area)
    (update-buttons! (get-selected-editor) text-area)))

(defn set-font-size!
  [text-area size]
  (.setFont text-area (-> text-area .getFont (.deriveFont (float size)))))

(defn save-font-size!
  [size]
  (utils/write-pref! :font-size size))

(defn decrease-font-size!
  [& _]
  (swap! font-size dec))

(defn increase-font-size!
  [& _]
  (swap! font-size inc))

(defn save-paredit!
  [enable?]
  (utils/write-pref! :enable-paredit enable?))

(defn save-doc!
  [enabled?]
  (utils/write-pref! :enable-doc enabled?))

(defn toggle-paredit!
  [& _]
  (reset! paredit-enabled? (not @paredit-enabled?)))

(defn toggle-doc!
  [& _]
  (reset! doc-enabled? (not @doc-enabled?)))

(defn show-paredit-help!
  [& _]
  (let [commands (->> pw/advanced-keymap
                      (apply concat)
                      (apply sorted-map-by #(compare %1 %2)))
        modifiers {"M" (utils/get-string :alt)
                   "C" (utils/get-string :ctrl)}]
    (->> (doseq [[k v] commands]
           (when-let [modifier (get modifiers (first k))]
             (println modifier "+" (second k) " " (name v))))
         with-out-str
         s/alert)))

(defn focus-on-field!
  [id]
  (when-let [editor (get-selected-editor)]
    (when-let [widget (s/select editor [id])]
      (doto widget
        s/request-focus!
        .selectAll))))

(defn focus-on-find!
  [& _]
  (focus-on-field! :#find))

(defn focus-on-replace!
  [& _]
  (focus-on-field! :#replace))

(defn find-text!
  [e]
  (when-let [text-area (get-selected-text-area)]
    (let [key-code (.getKeyCode e)
          enter-key? (= key-code 10)
          find-text (s/text e)
          printable-char? (-> text-area .getFont (.canDisplay key-code))
          valid-search? (and (> (count find-text) 0)
                             printable-char?
                             (not @shortcuts/down?)
                             (not= (.getKeyCode e) KeyEvent/VK_SHIFT))
          context (SearchContext. find-text)]
      (when valid-search?
        (.setRegularExpression context true)
        (when-not enter-key?
          (.setCaretPosition text-area 0))
        (when (.isShiftDown e)
          (.setSearchForward context false)))
      (if (and valid-search?
               (not (try (SearchEngine/find text-area context)
                      (catch Exception e false))))
        (s/config! e :background (color/color :red))
        (s/config! e :background nil)))))

(defn replace-text!
  [e]
  (when-let [text-area (get-selected-text-area)]
    (let [key-code (.getKeyCode e)
          enter-key? (= key-code 10)
          editor (get-selected-editor)
          find-text (s/text (s/select editor [:#find]))
          replace-text (s/text e)
          context (SearchContext. find-text)]
      (.setReplaceWith context replace-text)
      (if (and enter-key?
               (or (= (count find-text) 0)
                   (not (try (SearchEngine/replaceAll text-area context)
                          (catch Exception e false)))))
        (s/config! e :background (color/color :red))
        (s/config! e :background nil))
      (when enter-key?
        (update-buttons! editor text-area)))))

; create and display editors

(def ^:const clojure-exts #{"clj" "cljs" "cljx" "edn"})
(def ^:const wrap-exts #{"md" "txt"})
(def ^:const completer-keys #{KeyEvent/VK_ENTER
                              KeyEvent/VK_UP
                              KeyEvent/VK_DOWN
                              KeyEvent/VK_ESCAPE})
(def ^:const console-ignore-shortcut-keys #{KeyEvent/VK_Z
                                            KeyEvent/VK_Y})

(defn init-paredit!
  [text-area enable-default? enable-advanced?]
  (let [toggle-paredit-fn! (pw/init-paredit! text-area enable-default?)]
    (toggle-paredit-fn! (and enable-advanced? @paredit-enabled?))
    (when enable-advanced? toggle-paredit-fn!)))

(defn add-watchers!
  [path extension text-area completer]
  (let [clojure? (contains? clojure-exts extension)
        clojure-file? (and clojure? (.isFile (io/file path)))]
    (add-watch font-size
               (utils/hashed-keyword path)
               (fn [_ _ _ x]
                 (set-font-size! text-area x)))
    (when completer
      (add-watch doc-enabled?
                 (utils/hashed-keyword path)
                 (fn [_ _ _ enable?]
                   (.setAutoActivationEnabled completer enable?))))
    (when-let [toggle-fn! (init-paredit! text-area clojure-file? clojure?)]
      (add-watch paredit-enabled?
                 (utils/hashed-keyword path)
                 (fn [_ _ _ enable?]
                   (toggle-fn! enable?))))))

(defn add-button-watchers!
  [path pane]
  (add-watch doc-enabled?
             (utils/hashed-keyword (str "button:" path))
             (fn [_ _ _ enable?]
               (some-> (s/select pane [:#doc])
                       (s/config! :selected? enable?))))
  (add-watch paredit-enabled?
             (utils/hashed-keyword (str "button:" path))
             (fn [_ _ _ enable?]
               (some-> (s/select pane [:#paredit])
                       (s/config! :selected? enable?)))))

(defn remove-watchers!
  [path]
  (remove-watch font-size (utils/hashed-keyword path))
  (remove-watch paredit-enabled? (utils/hashed-keyword path))
  (remove-watch doc-enabled? (utils/hashed-keyword path))
  (remove-watch paredit-enabled? (utils/hashed-keyword (str "button:" path)))
  (remove-watch doc-enabled? (utils/hashed-keyword (str "button:" path))))

(defn apply-settings!
  [text-area]
  ; set theme
  (-> @theme-resource
      io/input-stream
      Theme/load
      (.apply text-area))
  ; set font size
  (->> (or @font-size (reset! font-size (-> text-area .getFont .getSize)))
       (set-font-size! text-area)))

(defn create-text-area
  ([]
    (doto (proxy [TextEditorPane] []
            (setMarginLineEnabled [enabled?]
              (proxy-super setMarginLineEnabled enabled?))
            (setMarginLinePosition [size]
              (proxy-super setMarginLinePosition size))
            (processKeyBinding [ks e condition pressed]
              (proxy-super processKeyBinding ks e condition pressed)))
      (.setAntiAliasingEnabled true)
      apply-settings!))
  ([path]
    (let [extension (utils/get-extension path)]
      (doto (create-text-area)
        (.load (FileLocation/create path) "UTF-8")
        .discardAllEdits
        (.setSyntaxEditingStyle (get utils/styles extension))
        (.setLineWrap (contains? wrap-exts extension))
        (.setMarginLineEnabled true)
        (.setMarginLinePosition 80)
        (.setTabSize (if (contains? clojure-exts extension) 2 4))))))

(defn get-completion-context
  [text-area prefix]
  (let [caretpos (.getCaretPosition text-area)
        all-text (.getText text-area)
        first-str (subs all-text 0 (- caretpos (count prefix)))
        second-str (subs all-text caretpos)]
    (-> (str first-str "__prefix__" second-str)
        parser/parse
        loc-utils/parsed-root-loc
        (static-analysis/top-level-code-form caretpos)
        first
        loc-utils/node-text
        read-string
        (try (catch Exception _)))))

(defn create-completion-provider
  [text-area extension]
  (cond
    ; clojure
    (contains? clojure-exts extension)
    (proxy [DefaultCompletionProvider] []
      (getCompletions [comp]
        (let [prefix (.getAlreadyEnteredText this comp)
              context (get-completion-context text-area prefix)]
          (for [symbol-str (compliment/completions prefix context)]
            (->> (str "<html><body><pre><span style='font-size: 11px;'>"
                      (compliment/documentation symbol-str)
                      "</span></pre></body></html>")
                 (BasicCompletion. this symbol-str nil)))))
      (isValidChar [ch]
        (or (Character/isLetterOrDigit ch)
            (contains? #{\* \+ \! \- \_ \? \/ \. \: \< \>} ch)))
      (isAutoActivateOkay [comp]
        true))
    ; anything else
    :else nil))

(defn create-completer
  [text-area extension]
  (when-let [provider (create-completion-provider text-area extension)]
    (doto (AutoCompletion. provider)
      (.setShowDescWindow true)
      (.setAutoCompleteSingleChoices false)
      (.setAutoCompleteEnabled true)
      (.setAutoActivationEnabled @doc-enabled?)
      (.setAutoActivationDelay 200)
      (.setChoicesWindowSize 150 300)
      (.setDescriptionWindowSize 600 300))))

(defn install-completer!
  [text-area completer]
  (.install completer text-area)<commit_msg>Don't make completions with nil<commit_after>(ns nightcode.editors
  (:require [clojure.java.io :as io]
            [compliment.core :as compliment]
            [flatland.ordered.map :as flatland]
            [nightcode.file-browser :as file-browser]
            [nightcode.shortcuts :as shortcuts]
            [nightcode.ui :as ui]
            [nightcode.utils :as utils]
            [paredit.loc-utils :as loc-utils]
            [paredit.static-analysis :as static-analysis]
            [paredit.parser :as parser]
            [paredit-widget.core :as pw]
            [seesaw.color :as color]
            [seesaw.core :as s])
  (:import [com.camick TextPrompt]
           [java.awt.event KeyEvent KeyListener]
           [javax.swing JComponent KeyStroke]
           [javax.swing.event DocumentListener HyperlinkEvent$EventType]
           [nightcode.ui JConsole]
           [org.fife.ui.autocomplete
            AutoCompletion BasicCompletion DefaultCompletionProvider]
           [org.fife.ui.rsyntaxtextarea FileLocation TextEditorPane Theme]
           [org.fife.ui.rtextarea RTextScrollPane SearchContext SearchEngine]))

(def editors (atom (flatland/ordered-map)))
(def font-size (atom (utils/read-pref :font-size)))
(def paredit-enabled? (atom (or (utils/read-pref :enable-paredit) false)))
(def doc-enabled? (atom (or (utils/read-pref :enable-doc) false)))
(def tabs (atom nil))
(def theme-resource (atom (io/resource "dark.xml")))

; basic getters

(defn get-text-area
  [view]
  (when view
    (->> [:<org.fife.ui.rsyntaxtextarea.TextEditorPane>]
         (s/select view)
         first)))

(defn get-text-area-from-path
  [path]
  (get-text-area (get-in @editors [path :view])))

(defn get-selected-text-area
  []
  (get-text-area-from-path @ui/tree-selection))

(defn get-selected-editor
  []
  (get-in @editors [@ui/tree-selection :view]))

(defn unsaved?
  [path]
  (when-let [text-area (get-text-area-from-path path)]
    (.isDirty text-area)))

(defn get-editor-text
  []
  (when-let [text-area (get-selected-text-area)]
    (.getText text-area)))

(defn get-editor-selected-text
  []
  (when-let [text-area (get-selected-text-area)]
    (.getSelectedText text-area)))

; tabs

(def ^:dynamic *reorder-tabs?* true)

(defn move-tab-selection!
  [diff]
  (let [paths (reverse (keys @editors))
        index (.indexOf paths @ui/tree-selection)
        max-index (- (count paths) 1)
        new-index (+ index diff)
        new-index (cond
                    (< new-index 0) max-index
                    (> new-index max-index) 0
                    :else new-index)]
    (when (> (count paths) 0)
      (binding [*reorder-tabs?* false]
        (ui/update-project-tree! (nth paths new-index)))))
  true)

(defn update-tabs!
  [path]
  (doto @ui/root .invalidate .validate)
  (let [editor-pane (ui/get-editor-pane)]
    (when @tabs (.closeBalloon @tabs))
    (->> (for [[e-path {:keys [italicize-fn]}] (reverse @editors)]
           (format "<a href='%s' style='text-decoration: %s;
                                        font-style: %s;'>%s</a>"
                   e-path
                   (if (utils/parent-path? path e-path) "underline" "none")
                   (if (italicize-fn) "italic" "normal")
                   (-> e-path io/file .getName)))
         (cons "<center>PgUp PgDn</center>")
         (clojure.string/join "<br/>")
         shortcuts/wrap-hint-text
         (s/editor-pane :editable? false :content-type "text/html" :text)
         (shortcuts/create-hint! true editor-pane)
         (reset! tabs))
    (s/listen (.getContents @tabs)
              :hyperlink
              (fn [e]
                (when (= (.getEventType e) HyperlinkEvent$EventType/ACTIVATED)
                  (binding [*reorder-tabs?* false]
                    (ui/update-project-tree! (.getDescription e))))))
    (shortcuts/toggle-hint! @tabs @shortcuts/down?)))

; button bar actions

(defn update-buttons!
  [editor ^TextEditorPane text-area]
  (when (ui/config! editor :#save :enabled? (.isDirty text-area))
    (update-tabs! @ui/tree-selection))
  (ui/config! editor :#undo :enabled? (.canUndo text-area))
  (ui/config! editor :#redo :enabled? (.canRedo text-area)))

(defn save-file!
  [& _]
  (when-let [text-area (get-selected-text-area)]
    (io!
      (with-open [w (io/writer (io/file @ui/tree-selection))]
        (.write text-area w)))
    (.setDirty text-area false)
    (s/request-focus! text-area)
    (update-buttons! (get-selected-editor) text-area))
  true)

(defn undo-file!
  [& _]
  (when-let [text-area (get-selected-text-area)]
    (.undoLastAction text-area)
    (update-buttons! (get-selected-editor) text-area)))

(defn redo-file!
  [& _]
  (when-let [text-area (get-selected-text-area)]
    (.redoLastAction text-area)
    (update-buttons! (get-selected-editor) text-area)))

(defn set-font-size!
  [text-area size]
  (.setFont text-area (-> text-area .getFont (.deriveFont (float size)))))

(defn save-font-size!
  [size]
  (utils/write-pref! :font-size size))

(defn decrease-font-size!
  [& _]
  (swap! font-size dec))

(defn increase-font-size!
  [& _]
  (swap! font-size inc))

(defn save-paredit!
  [enable?]
  (utils/write-pref! :enable-paredit enable?))

(defn save-doc!
  [enabled?]
  (utils/write-pref! :enable-doc enabled?))

(defn toggle-paredit!
  [& _]
  (reset! paredit-enabled? (not @paredit-enabled?)))

(defn toggle-doc!
  [& _]
  (reset! doc-enabled? (not @doc-enabled?)))

(defn show-paredit-help!
  [& _]
  (let [commands (->> pw/advanced-keymap
                      (apply concat)
                      (apply sorted-map-by #(compare %1 %2)))
        modifiers {"M" (utils/get-string :alt)
                   "C" (utils/get-string :ctrl)}]
    (->> (doseq [[k v] commands]
           (when-let [modifier (get modifiers (first k))]
             (println modifier "+" (second k) " " (name v))))
         with-out-str
         s/alert)))

(defn focus-on-field!
  [id]
  (when-let [editor (get-selected-editor)]
    (when-let [widget (s/select editor [id])]
      (doto widget
        s/request-focus!
        .selectAll))))

(defn focus-on-find!
  [& _]
  (focus-on-field! :#find))

(defn focus-on-replace!
  [& _]
  (focus-on-field! :#replace))

(defn find-text!
  [e]
  (when-let [text-area (get-selected-text-area)]
    (let [key-code (.getKeyCode e)
          enter-key? (= key-code 10)
          find-text (s/text e)
          printable-char? (-> text-area .getFont (.canDisplay key-code))
          valid-search? (and (> (count find-text) 0)
                             printable-char?
                             (not @shortcuts/down?)
                             (not= (.getKeyCode e) KeyEvent/VK_SHIFT))
          context (SearchContext. find-text)]
      (when valid-search?
        (.setRegularExpression context true)
        (when-not enter-key?
          (.setCaretPosition text-area 0))
        (when (.isShiftDown e)
          (.setSearchForward context false)))
      (if (and valid-search?
               (not (try (SearchEngine/find text-area context)
                      (catch Exception e false))))
        (s/config! e :background (color/color :red))
        (s/config! e :background nil)))))

(defn replace-text!
  [e]
  (when-let [text-area (get-selected-text-area)]
    (let [key-code (.getKeyCode e)
          enter-key? (= key-code 10)
          editor (get-selected-editor)
          find-text (s/text (s/select editor [:#find]))
          replace-text (s/text e)
          context (SearchContext. find-text)]
      (.setReplaceWith context replace-text)
      (if (and enter-key?
               (or (= (count find-text) 0)
                   (not (try (SearchEngine/replaceAll text-area context)
                          (catch Exception e false)))))
        (s/config! e :background (color/color :red))
        (s/config! e :background nil))
      (when enter-key?
        (update-buttons! editor text-area)))))

; create and display editors

(def ^:const clojure-exts #{"clj" "cljs" "cljx" "edn"})
(def ^:const wrap-exts #{"md" "txt"})
(def ^:const completer-keys #{KeyEvent/VK_ENTER
                              KeyEvent/VK_UP
                              KeyEvent/VK_DOWN
                              KeyEvent/VK_ESCAPE})
(def ^:const console-ignore-shortcut-keys #{KeyEvent/VK_Z
                                            KeyEvent/VK_Y})

(defn init-paredit!
  [text-area enable-default? enable-advanced?]
  (let [toggle-paredit-fn! (pw/init-paredit! text-area enable-default?)]
    (toggle-paredit-fn! (and enable-advanced? @paredit-enabled?))
    (when enable-advanced? toggle-paredit-fn!)))

(defn add-watchers!
  [path extension text-area completer]
  (let [clojure? (contains? clojure-exts extension)
        clojure-file? (and clojure? (.isFile (io/file path)))]
    (add-watch font-size
               (utils/hashed-keyword path)
               (fn [_ _ _ x]
                 (set-font-size! text-area x)))
    (when completer
      (add-watch doc-enabled?
                 (utils/hashed-keyword path)
                 (fn [_ _ _ enable?]
                   (.setAutoActivationEnabled completer enable?))))
    (when-let [toggle-fn! (init-paredit! text-area clojure-file? clojure?)]
      (add-watch paredit-enabled?
                 (utils/hashed-keyword path)
                 (fn [_ _ _ enable?]
                   (toggle-fn! enable?))))))

(defn add-button-watchers!
  [path pane]
  (add-watch doc-enabled?
             (utils/hashed-keyword (str "button:" path))
             (fn [_ _ _ enable?]
               (some-> (s/select pane [:#doc])
                       (s/config! :selected? enable?))))
  (add-watch paredit-enabled?
             (utils/hashed-keyword (str "button:" path))
             (fn [_ _ _ enable?]
               (some-> (s/select pane [:#paredit])
                       (s/config! :selected? enable?)))))

(defn remove-watchers!
  [path]
  (remove-watch font-size (utils/hashed-keyword path))
  (remove-watch paredit-enabled? (utils/hashed-keyword path))
  (remove-watch doc-enabled? (utils/hashed-keyword path))
  (remove-watch paredit-enabled? (utils/hashed-keyword (str "button:" path)))
  (remove-watch doc-enabled? (utils/hashed-keyword (str "button:" path))))

(defn apply-settings!
  [text-area]
  ; set theme
  (-> @theme-resource
      io/input-stream
      Theme/load
      (.apply text-area))
  ; set font size
  (->> (or @font-size (reset! font-size (-> text-area .getFont .getSize)))
       (set-font-size! text-area)))

(defn create-text-area
  ([]
    (doto (proxy [TextEditorPane] []
            (setMarginLineEnabled [enabled?]
              (proxy-super setMarginLineEnabled enabled?))
            (setMarginLinePosition [size]
              (proxy-super setMarginLinePosition size))
            (processKeyBinding [ks e condition pressed]
              (proxy-super processKeyBinding ks e condition pressed)))
      (.setAntiAliasingEnabled true)
      apply-settings!))
  ([path]
    (let [extension (utils/get-extension path)]
      (doto (create-text-area)
        (.load (FileLocation/create path) "UTF-8")
        .discardAllEdits
        (.setSyntaxEditingStyle (get utils/styles extension))
        (.setLineWrap (contains? wrap-exts extension))
        (.setMarginLineEnabled true)
        (.setMarginLinePosition 80)
        (.setTabSize (if (contains? clojure-exts extension) 2 4))))))

(defn get-completion-context
  [text-area prefix]
  (let [caretpos (.getCaretPosition text-area)
        all-text (.getText text-area)
        first-str (subs all-text 0 (- caretpos (count prefix)))
        second-str (subs all-text caretpos)]
    (-> (str first-str "__prefix__" second-str)
        parser/parse
        loc-utils/parsed-root-loc
        (static-analysis/top-level-code-form caretpos)
        first
        loc-utils/node-text
        read-string
        (try (catch Exception _)))))

(defn create-completion-provider
  [text-area extension]
  (cond
    ; clojure
    (contains? clojure-exts extension)
    (proxy [DefaultCompletionProvider] []
      (getCompletions [comp]
        (let [prefix (.getAlreadyEnteredText this comp)
              context (get-completion-context text-area prefix)]
          (for [symbol-str (compliment/completions prefix context)
                :when (some? symbol-str)]
            (->> (str "<html><body><pre><span style='font-size: 11px;'>"
                      (compliment/documentation symbol-str)
                      "</span></pre></body></html>")
                 (BasicCompletion. this symbol-str nil)))))
      (isValidChar [ch]
        (or (Character/isLetterOrDigit ch)
            (contains? #{\* \+ \! \- \_ \? \/ \. \: \< \>} ch)))
      (isAutoActivateOkay [comp]
        true))
    ; anything else
    :else nil))

(defn create-completer
  [text-area extension]
  (when-let [provider (create-completion-provider text-area extension)]
    (doto (AutoCompletion. provider)
      (.setShowDescWindow true)
      (.setAutoCompleteSingleChoices false)
      (.setAutoCompleteEnabled true)
      (.setAutoActivationEnabled @doc-enabled?)
      (.setAutoActivationDelay 200)
      (.setChoicesWindowSize 150 300)
      (.setDescriptionWindowSize 600 300))))

(defn install-completer!
  [text-area completer]
  (.install completer text-area)
{'fraction_non_alphanumeric': 0.15088634387874664, 'fraction_numerical': 0.0027213969837850097, 'mean_word_length': 2.2499692912418623, 'pattern_counts': {'":': 0, '<': 35, '<?xml version=': 0, '>': 79, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 2}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4871733', 'n_tokens_mistral': 8926, 'n_tokens_neox': 8630, 'n_words': 1910}