feat: v4.6.0 — AI assistant, collaboration, knowledge base, and 15 more features

- AI Assistant plugin: multi-provider chat (OpenAI/Anthropic/Ollama/LM Studio),
  summarize/improve/translate commands, proofread via ai:analyze; calls
  proxied through main so API keys stay out of the renderer
- Collaboration plugin: anchor-based comments in .comments/ sidecars with
  drift detection and F8 navigation
- Local knowledge base: [[wiki-links]] with click-to-create + Backlinks panel
- Crash recovery: debounced session snapshots with restore prompt on launch
- Version history: pre-save snapshots, History panel with restore/diff/delete
- Real PDF encryption: swap pdf-lib for @cantoo/pdf-lib (probe-driven UI)
- XLSX export (native workbooks via JSZip), ODT headers/footers + page size
- Offline KaTeX (bundled CSS+fonts), local-first PlantUML rendering
- Editor: vim mode toggle, snippet Tab-expansion, zen word-goal setter,
  writing heatmap, writing-studio panels wired with rail icons
- Quick Note global scratchpad (Ctrl+Alt+Q), markdownconverter:// deep links,
  REPL first-run confirmation
- Fix: Ctrl+Shift+P collision, pandoc converter availability check, CLI
  dangling --css/--reference-doc flags, dead converter button

8 new test suites; 613 tests green; lint clean
This commit is contained in:
2026-09-05 20:48:39 +05:30
parent b83ba91731
commit c4dcbd8caf
105 changed files with 5695 additions and 195 deletions
+64 -2
View File
@@ -8,7 +8,7 @@ const {
highlightActiveLine,
drawSelection,
} = require('@codemirror/view');
const { EditorState } = require('@codemirror/state');
const { EditorState, Compartment } = require('@codemirror/state');
const { markdown, markdownLanguage } = require('@codemirror/lang-markdown');
// Language extensions loaded lazily on first use
let _javascript, _html, _css, _json, _python;
@@ -18,6 +18,30 @@ const { autocompletion, completionKeymap } = require('@codemirror/autocomplete')
const { bracketMatching, foldGutter, indentOnInput } = require('@codemirror/language');
const { oneDark } = require('@codemirror/theme-one-dark');
// Compartments allow toggling features (vim mode) on a live editor without
// recreating the view — CodeMirror 6's intended mechanism for reconfiguration.
const vimCompartment = new Compartment();
// Vim is loaded lazily: most users never enable it, and the module is heavy.
let _vimExtension = null;
function getVimExtension() {
if (!_vimExtension) {
_vimExtension = require('@replit/codemirror-vim').vim();
}
return _vimExtension;
}
/**
* Toggle vim keybindings on an editor created by createEditor.
* @param {EditorView} view
* @param {boolean} enabled
*/
function setVimMode(view, enabled) {
if (!view) return;
view.dispatch({
effects: vimCompartment.reconfigure(enabled ? getVimExtension() : []),
});
}
// Custom theme for JetBrains Mono font
const jetBrainsMonoTheme = EditorView.theme({
'&': {
@@ -31,6 +55,37 @@ const jetBrainsMonoTheme = EditorView.theme({
},
});
/**
* Build the Tab keymap entry that expands snippets before falling through to
* indent. `getExpansion(prefix)` is synchronous and returns the replacement
* text (or null to decline), letting the host keep its snippets in a Map.
*/
function snippetTabKeymap(getExpansion) {
return {
key: 'Tab',
run: (view) => {
if (typeof getExpansion !== 'function') return false;
const { head } = view.state.selection.main;
const line = view.state.doc.lineAt(head);
// Word-before-cursor: letters, digits, underscore and hyphen
const before = line.text.slice(0, head - line.from);
const match = /[\w-]+$/.exec(before);
if (!match) return false;
const expansion = getExpansion(match[0].toLowerCase());
if (typeof expansion !== 'string') return false;
view.dispatch({
changes: {
from: head - match[0].length,
to: head,
insert: expansion,
},
selection: { anchor: head - match[0].length + expansion.length },
});
return true;
},
};
}
/**
* Create a CodeMirror 6 editor instance.
*
@@ -41,6 +96,8 @@ const jetBrainsMonoTheme = EditorView.theme({
* @param {Function} options.onUpdate - called with the EditorView on every update (selection, doc change, etc.)
* @param {boolean} options.isDark - apply oneDark theme when true (default false)
* @param {boolean} options.showLineNumbers - show line-number gutter (default true)
* @param {boolean} options.vimMode - start with vim keybindings (default false; toggle later via setVimMode)
* @param {Function} options.getTabExpansion - (prefix)=>string|null; Tab expands a matching snippet (default null)
* @returns {EditorView} the created editor view
*/
function createEditor(parentElement, options = {}) {
@@ -62,6 +119,8 @@ function createEditor(parentElement, options = {}) {
onUpdate = null,
isDark = false,
showLineNumbers = true,
vimMode = false,
getTabExpansion = null,
} = options;
const extensions = [
@@ -75,13 +134,16 @@ function createEditor(parentElement, options = {}) {
autocompletion(),
foldGutter(),
jetBrainsMonoTheme,
// Snippet Tab expansion must precede indentWithTab so it wins when matched
keymap.of([
...(getTabExpansion ? [snippetTabKeymap(getTabExpansion)] : []),
...defaultKeymap,
...historyKeymap,
...searchKeymap,
...completionKeymap,
indentWithTab,
]),
vimCompartment.of(vimMode ? getVimExtension() : []),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
onChange(update.state.doc.toString());
@@ -146,4 +208,4 @@ function getLanguageExtension(lang) {
return loader ? loader() : markdown({ base: markdownLanguage });
}
module.exports = { createEditor, getLanguageExtension };
module.exports = { createEditor, getLanguageExtension, setVimMode, snippetTabKeymap };