+
-
+
diff --git a/src/main.js b/src/main.js
index 38fe351..81cf290 100644
--- a/src/main.js
+++ b/src/main.js
@@ -266,6 +266,7 @@ class AppController {
#renderCurrentFile() {
const path = this.projectStore.selectedFilePath;
if (!path) {
+ this.view.setEditorLanguage("");
this.view.renderFile("");
this.view.renderMarkdown("");
this.view.setMarkdownToggleVisible(false);
@@ -277,6 +278,7 @@ class AppController {
const file = this.projectStore.getSelectedFile();
const content = this.draftByPath.get(path) ?? file?.content ?? "";
const isMarkdown = this.#isMarkdownPath(path);
+ this.view.setEditorLanguage(path);
if (isMarkdown) {
const mode = this.markdownModeByPath.get(path) || "preview";
diff --git a/src/ui/AppView.js b/src/ui/AppView.js
index 530cc97..e472da7 100644
--- a/src/ui/AppView.js
+++ b/src/ui/AppView.js
@@ -1,6 +1,10 @@
+import { MonacoEditorAdapter } from "./MonacoEditorAdapter.js";
+
export class AppView {
constructor(reviewStore) {
this.reviewStore = reviewStore;
+ this.editorInputHandler = null;
+ this.monacoAdapter = null;
this.el = {
layout: document.getElementById("layout-root"),
splitterLeft: document.getElementById("splitter-left"),
@@ -14,6 +18,7 @@ export class AppView {
newTextTabBtn: document.getElementById("new-text-tab"),
mdToggleBtn: document.getElementById("md-toggle-mode"),
fileEditor: document.getElementById("file-editor"),
+ fileEditorMonaco: document.getElementById("file-editor-monaco"),
mdPreview: document.getElementById("md-preview"),
editorInfo: document.getElementById("editor-info"),
saveFileBtn: document.getElementById("save-file"),
@@ -27,6 +32,7 @@ export class AppView {
chatForm: document.getElementById("chat-form"),
chatInput: document.getElementById("chat-input")
};
+ void this.#initMonacoEditor();
}
setIndexStatus(text) {
@@ -48,10 +54,13 @@ export class AppView {
setEditorEnabled(enabled) {
this.el.fileEditor.readOnly = !enabled;
+ if (this.monacoAdapter) this.monacoAdapter.setReadOnly(!enabled);
}
bindEditorInput(onInput) {
+ this.editorInputHandler = onInput;
this.el.fileEditor.oninput = () => onInput(this.el.fileEditor.value);
+ if (this.monacoAdapter) this.monacoAdapter.onChange(onInput);
}
bindMarkdownToggle(onToggle) {
@@ -85,7 +94,7 @@ export class AppView {
this.el.mdToggleBtn.classList.toggle("hidden", !visible);
if (!visible) {
this.el.mdPreview.classList.add("hidden");
- this.el.fileEditor.classList.remove("hidden");
+ this.#setEditorVisible(true);
}
}
@@ -95,7 +104,11 @@ export class AppView {
this.el.mdToggleBtn.textContent = isPreview ? "✏️" : "👁";
this.el.mdToggleBtn.title = isPreview ? "Перейти в редактирование" : "Перейти в просмотр";
this.el.mdPreview.classList.toggle("hidden", !isPreview);
- this.el.fileEditor.classList.toggle("hidden", isPreview);
+ this.#setEditorVisible(!isPreview);
+ if (!isPreview && this.monacoAdapter) {
+ this.monacoAdapter.layout();
+ this.monacoAdapter.focus();
+ }
}
renderMarkdown(html) {
@@ -175,6 +188,11 @@ export class AppView {
renderFile(content) {
this.el.fileEditor.value = content || "";
+ if (this.monacoAdapter) this.monacoAdapter.setValue(content || "");
+ }
+
+ setEditorLanguage(path) {
+ if (this.monacoAdapter) this.monacoAdapter.setLanguageByPath(path || "");
}
renderChanges(changes, activePath, onPick) {
@@ -224,4 +242,31 @@ export class AppView {
this.el.diffView.appendChild(row);
}
}
+
+ async #initMonacoEditor() {
+ if (!this.el.fileEditorMonaco) return;
+ const adapter = new MonacoEditorAdapter(this.el.fileEditorMonaco);
+ const ready = await adapter.init();
+ if (!ready) return;
+
+ this.monacoAdapter = adapter;
+ const editorVisible = !this.el.fileEditor.classList.contains("hidden");
+ this.el.fileEditorMonaco.classList.toggle("hidden", !editorVisible);
+ this.el.fileEditor.classList.add("hidden");
+ this.monacoAdapter.setReadOnly(this.el.fileEditor.readOnly);
+ this.monacoAdapter.setValue(this.el.fileEditor.value || "");
+ if (this.editorInputHandler) {
+ this.monacoAdapter.onChange(this.editorInputHandler);
+ }
+ }
+
+ #setEditorVisible(visible) {
+ if (this.monacoAdapter) {
+ this.el.fileEditorMonaco.classList.toggle("hidden", !visible);
+ this.el.fileEditor.classList.add("hidden");
+ if (visible) this.monacoAdapter.layout();
+ return;
+ }
+ this.el.fileEditor.classList.toggle("hidden", !visible);
+ }
}
diff --git a/src/ui/MonacoEditorAdapter.js b/src/ui/MonacoEditorAdapter.js
new file mode 100644
index 0000000..b49cb13
--- /dev/null
+++ b/src/ui/MonacoEditorAdapter.js
@@ -0,0 +1,146 @@
+export class MonacoEditorAdapter {
+ constructor(hostElement) {
+ this.hostElement = hostElement;
+ this.editor = null;
+ this.model = null;
+ this.changeHandler = null;
+ this.suppressChange = false;
+ this.readOnly = true;
+ }
+
+ async init() {
+ const monaco = await this.#loadMonaco();
+ if (!monaco || !this.hostElement) return false;
+
+ this.#defineTheme(monaco);
+ this.model = monaco.editor.createModel("", "plaintext");
+ this.editor = monaco.editor.create(this.hostElement, {
+ model: this.model,
+ theme: "app-dark-blue",
+ automaticLayout: true,
+ readOnly: this.readOnly,
+ minimap: { enabled: false },
+ scrollBeyondLastLine: false,
+ fontSize: 14,
+ fontFamily: "IBM Plex Mono, Consolas, monospace",
+ tabSize: 2
+ });
+
+ this.model.onDidChangeContent(() => {
+ if (this.suppressChange || !this.changeHandler) return;
+ this.changeHandler(this.model.getValue());
+ });
+
+ return true;
+ }
+
+ onChange(handler) {
+ this.changeHandler = handler;
+ }
+
+ setReadOnly(readOnly) {
+ this.readOnly = Boolean(readOnly);
+ if (!this.editor) return;
+ this.editor.updateOptions({ readOnly: this.readOnly });
+ }
+
+ setValue(text) {
+ if (!this.model) return;
+ const next = text || "";
+ if (this.model.getValue() === next) return;
+ this.suppressChange = true;
+ this.model.pushEditOperations(
+ [],
+ [{ range: this.model.getFullModelRange(), text: next }],
+ () => null
+ );
+ this.model.pushStackElement();
+ this.suppressChange = false;
+ }
+
+ setLanguageByPath(path) {
+ if (!this.model || !window.monaco?.editor) return;
+ window.monaco.editor.setModelLanguage(this.model, this.#detectLanguage(path));
+ }
+
+ focus() {
+ if (this.editor) this.editor.focus();
+ }
+
+ layout() {
+ if (this.editor) this.editor.layout();
+ }
+
+ async #loadMonaco() {
+ if (window.monaco?.editor) return window.monaco;
+ if (typeof window.require !== "function") return null;
+ if (window.__monacoPromise) return window.__monacoPromise;
+
+ window.__monacoPromise = new Promise((resolve, reject) => {
+ window.require.config({
+ paths: {
+ vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.2/min/vs"
+ }
+ });
+ window.require(["vs/editor/editor.main"], () => resolve(window.monaco), reject);
+ });
+
+ return window.__monacoPromise;
+ }
+
+ #defineTheme(monaco) {
+ monaco.editor.defineTheme("app-dark-blue", {
+ base: "vs-dark",
+ inherit: true,
+ rules: [
+ { token: "comment", foreground: "7f9bbf" },
+ { token: "keyword", foreground: "8cc8ff" },
+ { token: "string", foreground: "a7d98f" }
+ ],
+ colors: {
+ "editor.background": "#0b1830",
+ "editor.foreground": "#dce9ff",
+ "editorLineNumber.foreground": "#5e79a2",
+ "editorLineNumber.activeForeground": "#a7c3eb",
+ "editor.selectionBackground": "#274f8677",
+ "editor.inactiveSelectionBackground": "#274f8644",
+ "editorCursor.foreground": "#4fa0ff"
+ }
+ });
+ }
+
+ #detectLanguage(path) {
+ const ext = (path || "").split(".").pop()?.toLowerCase();
+ switch (ext) {
+ case "js":
+ case "mjs":
+ case "cjs":
+ return "javascript";
+ case "ts":
+ return "typescript";
+ case "json":
+ return "json";
+ case "md":
+ case "markdown":
+ return "markdown";
+ case "yml":
+ case "yaml":
+ return "yaml";
+ case "xml":
+ return "xml";
+ case "html":
+ return "html";
+ case "css":
+ return "css";
+ case "sh":
+ case "bash":
+ return "shell";
+ case "py":
+ return "python";
+ case "toml":
+ return "ini";
+ default:
+ return "plaintext";
+ }
+ }
+}
diff --git a/styles.css b/styles.css
index 4dda90a..9298802 100644
--- a/styles.css
+++ b/styles.css
@@ -316,17 +316,17 @@ textarea {
}
.md-toggle {
- position: absolute;
- top: 8px;
- right: 8px;
- z-index: 2;
- width: 30px;
- height: 30px;
+ position: static;
+ z-index: 0;
+ width: var(--top-control-btn-h);
+ min-width: var(--top-control-btn-h);
+ height: var(--top-control-btn-h);
padding: 0;
border-radius: 999px;
- font-size: 15px;
+ font-size: 16px;
line-height: 1;
background: #163057;
+ flex-shrink: 0;
}
.md-toggle.active {
@@ -355,6 +355,11 @@ textarea {
flex: 1;
}
+.file-editor.monaco-host {
+ padding: 0;
+ overflow: hidden;
+}
+
.md-preview {
background: #0b1830;
border: 1px solid var(--line);