From 5add7ef706cfae1b53a340ca32d14bf6412006dc Mon Sep 17 00:00:00 2001 From: Dylan Armstrong Date: Mon, 26 May 2025 19:57:26 +0200 Subject: [PATCH] feat: update editor directly to fix jumping cursor --- manifest.json | 2 +- package.json | 3 ++ pnpm-lock.yaml | 19 +++++++++ src/@types/obsidian-ex.d.ts | 5 +++ src/prettier-plugin.ts | 84 ++++++++++++++++++++++++------------- tsconfig.json | 1 + 6 files changed, 84 insertions(+), 30 deletions(-) diff --git a/manifest.json b/manifest.json index 6cef1ef..63ce08a 100644 --- a/manifest.json +++ b/manifest.json @@ -7,5 +7,5 @@ "isDesktopOnly": false, "minAppVersion": "0.15.0", "name": "Format Automatically with Prettier", - "version": "0.1.8" + "version": "0.2.0" } diff --git a/package.json b/package.json index 4f7e2a2..6fdbf01 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,16 @@ { "author": "Dylan Armstrong", "dependencies": { + "diff-match-patch": "^1.0.5", "obsidian": "^1.8.7", "prettier": "^3.5.3" }, "description": "This is a formatting plugin for Obsidian (https://obsidian.md)", "devDependencies": { + "@codemirror/view": "^6.36.8", "@dylanarmstrong/eslint-config": "^0.8.0", "@eslint/js": "^9.27.0", + "@types/diff-match-patch": "^1.0.36", "@types/node": "^22.15.21", "builtin-modules": "5.0.0", "esbuild": "0.25.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3390fb..b3d79ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: dependencies: + diff-match-patch: + specifier: ^1.0.5 + version: 1.0.5 obsidian: specifier: ^1.8.7 version: 1.8.7(@codemirror/state@6.5.2)(@codemirror/view@6.36.8) @@ -15,12 +18,18 @@ importers: specifier: ^3.5.3 version: 3.5.3 devDependencies: + '@codemirror/view': + specifier: ^6.36.8 + version: 6.36.8 '@dylanarmstrong/eslint-config': specifier: ^0.8.0 version: 0.8.0(eslint@9.27.0) '@eslint/js': specifier: ^9.27.0 version: 9.27.0 + '@types/diff-match-patch': + specifier: ^1.0.36 + version: 1.0.36 '@types/node': specifier: ^22.15.21 version: 22.15.21 @@ -301,6 +310,9 @@ packages: '@types/codemirror@5.60.8': resolution: {integrity: sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==} + '@types/diff-match-patch@1.0.36': + resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} + '@types/estree@1.0.7': resolution: {integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==} @@ -454,6 +466,9 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + diff-match-patch@1.0.5: + resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} + electron-to-chromium@1.5.157: resolution: {integrity: sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==} @@ -1036,6 +1051,8 @@ snapshots: dependencies: '@types/tern': 0.23.9 + '@types/diff-match-patch@1.0.36': {} + '@types/estree@1.0.7': {} '@types/json-schema@7.0.15': {} @@ -1209,6 +1226,8 @@ snapshots: deep-is@0.1.4: {} + diff-match-patch@1.0.5: {} + electron-to-chromium@1.5.157: {} esbuild@0.25.4: diff --git a/src/@types/obsidian-ex.d.ts b/src/@types/obsidian-ex.d.ts index c65ae13..c23f6e9 100644 --- a/src/@types/obsidian-ex.d.ts +++ b/src/@types/obsidian-ex.d.ts @@ -1,4 +1,5 @@ import 'obsidian'; +import { EditorView } from '@codemirror/view'; declare module 'obsidian' { interface App { @@ -12,6 +13,10 @@ declare module 'obsidian' { }; } + interface Editor { + cm?: EditorView; + } + interface Vault { getConfig(id: string): boolean; } diff --git a/src/prettier-plugin.ts b/src/prettier-plugin.ts index c3b6372..3d3939e 100644 --- a/src/prettier-plugin.ts +++ b/src/prettier-plugin.ts @@ -1,3 +1,4 @@ +import DiffMatchPatch from 'diff-match-patch'; import { EditorPosition, Plugin } from 'obsidian'; import * as markdownPlugin from 'prettier/plugins/markdown'; @@ -17,22 +18,11 @@ const DEFAULT_SETTINGS: PrettierPluginSettings = { useTabs: true, }; -// Taken from https://github.com/alexgavrusev/obsidian-format-with-prettier/blob/master/src/cursor-position-utils.ts -// If the cursor is on line i, on character j, the offset -// is j plus sum of lengths (incl. the \n at the end) of lines 0..(i - 1) -const toOffset = (position: EditorPosition, text: string): number => - text - .split('\n') - .slice(0, position.line) - .reduce((accumulator, line) => accumulator + line.length + 1, position.ch); - -const fromOffset = (offset: number, text: string): EditorPosition => { - const textUpToOffset = text.slice(0, offset); - const newLines = textUpToOffset.split('\n'); - +const getPosition = (text: string): EditorPosition => { + const lines = text.split('\n'); return { - ch: newLines?.at(-1)?.length || 0, - line: newLines.length - 1, + ch: lines.at(-1)?.length ?? 0, + line: lines.length - 1, }; }; @@ -43,28 +33,64 @@ export default class PrettierPlugin extends Plugin { private async format() { const editor = this.app.workspace.activeEditor?.editor; - if (!editor) { + if (!editor || !editor.cm) { return; } const file = this.app.workspace.getActiveFile(); const text = editor.getValue(); if (text && file) { - const position = editor.getCursor(); - const cursorOffset = toOffset(position, text); + const formattedText = await prettier.format(text, { + embeddedLanguageFormatting: 'auto', + filepath: file.path, + parser: 'markdown', + plugins: [markdownPlugin], + tabWidth: this.settings.tabWidth, + useTabs: this.settings.useTabs, + }); - const { cursorOffset: formattedCursorOffset, formatted: formattedText } = - await prettier.formatWithCursor(text, { - cursorOffset, - filepath: file.path, - parser: 'markdown', - plugins: [markdownPlugin], - tabWidth: this.settings.tabWidth, - useTabs: this.settings.useTabs, - }); + // Switched to using DiffMatchPatch after I saw it here: + // https://github.com/platers/obsidian-linter + // And this style of updating the editor stops my cursor from jumping + // around. Also, it keeps sections toggled if I've closed them. - editor.setValue(formattedText); - editor.setCursor(fromOffset(formattedCursorOffset, formattedText)); + // eslint-disable-next-line new-cap + const dmp = new DiffMatchPatch.diff_match_patch(); + const changes = dmp.diff_main(text, formattedText); + + let currentText = ''; + for (const change of changes) { + const [type, value] = change; + + if (type === DiffMatchPatch.DIFF_INSERT) { + editor.cm.dispatch({ + changes: [ + { + from: editor.posToOffset(getPosition(currentText)), + insert: value, + }, + ], + filter: false, + }); + currentText += value; + } else if (type === DiffMatchPatch.DIFF_DELETE) { + const start = getPosition(currentText); + const end = getPosition(currentText + value); + + editor.cm.dispatch({ + changes: [ + { + from: editor.posToOffset(start), + insert: '', + to: editor.posToOffset(end), + }, + ], + filter: false, + }); + } else { + currentText += value; + } + } } } diff --git a/tsconfig.json b/tsconfig.json index 3609678..dee4989 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "allowJs": true, + "allowSyntheticDefaultImports": true, "baseUrl": ".", "importHelpers": true, "inlineSourceMap": true,