Repository: nadavspi/obsidian-relative-line-numbers Branch: main Commit: 1c2aeace65b8 Files: 16 Total size: 11.2 KB Directory structure: gitextract_lnic64ha/ ├── .envrc ├── .github/ │ └── workflows/ │ └── release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── README.md ├── esbuild.config.mjs ├── extension.ts ├── flake.nix ├── main.ts ├── manifest.json ├── package.json ├── styles.css ├── tsconfig.json ├── version-bump.mjs └── versions.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .envrc ================================================ use flake ================================================ FILE: .github/workflows/release.yml ================================================ name: Release Obsidian plugin on: push: tags: - "*" jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: "18.x" - name: Build plugin run: | npm install npm run build - name: Create release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | tag="${GITHUB_REF#refs/tags/}" gh release create "$tag" \ --title="$tag" \ --draft \ main.js manifest.json styles.css ================================================ FILE: .gitignore ================================================ # Intellij *.iml .idea # npm node_modules package-lock.json # build main.js *.js.map ================================================ FILE: .prettierignore ================================================ # build main.js *.js.map ================================================ FILE: .prettierrc.json ================================================ { "semi": true } ================================================ FILE: README.md ================================================ ## Obsidian Relative Line Numbers Plugin ![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nadavspi/obsidian-relative-line-numbers?style=for-the-badge) ![GitHub all releases](https://img.shields.io/github/downloads/nadavspi/obsidian-relative-line-numbers/total?style=for-the-badge) This [Obsidian](https://obsidian.md/) plugin enables relative line numbers (similar to Vim's relativenumber) in editor mode. Note: the "Show line number" setting must be enabled in Editor options. ![](demo.gif) ================================================ FILE: esbuild.config.mjs ================================================ import esbuild from "esbuild"; import process from "process"; import builtins from 'builtin-modules' const banner = `/* THIS IS A GENERATED/BUNDLED FILE BY ESBUILD if you want to view the source, please visit the github repository of this plugin */ `; const prod = (process.argv[2] === 'production'); esbuild.build({ banner: { js: banner, }, entryPoints: ['main.ts'], bundle: true, external: [ 'obsidian', 'electron', '@codemirror/autocomplete', '@codemirror/closebrackets', '@codemirror/collab', '@codemirror/commands', '@codemirror/comment', '@codemirror/fold', '@codemirror/gutter', '@codemirror/highlight', '@codemirror/history', '@codemirror/language', '@codemirror/lint', '@codemirror/matchbrackets', '@codemirror/panel', '@codemirror/rangeset', '@codemirror/rectangular-selection', '@codemirror/search', '@codemirror/state', '@codemirror/stream-parser', '@codemirror/text', '@codemirror/tooltip', '@codemirror/view', ...builtins], format: 'cjs', watch: !prod, target: 'es2016', logLevel: "info", sourcemap: prod ? false : 'inline', treeShaking: true, outfile: 'main.js', }).catch(() => process.exit(1)); ================================================ FILE: extension.ts ================================================ import { Extension } from "@codemirror/state"; import { EditorView, ViewUpdate, gutter, lineNumbers, GutterMarker } from "@codemirror/view"; import { Compartment, EditorState } from "@codemirror/state"; import {foldedRanges} from "@codemirror/language" let relativeLineNumberGutter = new Compartment(); class Marker extends GutterMarker { /** The text to render in gutter */ text: string; constructor(text: string) { super(); this.text = text; this.elementClass = "relative-line-numbers-mono"; } toDOM() { return document.createTextNode(this.text); } } function linesCharLength(state: EditorState): number { /** * Get the character length of the number of lines in the document * Example: 100 lines -> 3 characters */ return state.doc.lines.toString().length; } const absoluteLineNumberGutter = gutter({ lineMarker: (view, line) => { const lineNo = view.state.doc.lineAt(line.from).number; const charLength = linesCharLength(view.state); const absoluteLineNo = new Marker(lineNo.toString().padStart(charLength, " ")); const cursorLine = view.state.doc.lineAt( view.state.selection.asSingle().ranges[0].to ).number; if (lineNo === cursorLine) { return absoluteLineNo; } return null; }, initialSpacer: (view: EditorView) => { const spacer = new Marker("0".repeat(linesCharLength(view.state))); return spacer; }, }); function relativeLineNumbers(lineNo: number, state: EditorState) { const charLength = linesCharLength(state); const blank = " ".padStart(charLength, " "); if (lineNo > state.doc.lines) { return blank; } const cursorLine = state.doc.lineAt( state.selection.asSingle().ranges[0].to ).number; const start = Math.min( state.doc.line(lineNo).from, state.selection.asSingle().ranges[0].to) const stop = Math.max( state.doc.line(lineNo).from, state.selection.asSingle().ranges[0].to) const folds = foldedRanges(state) let foldedCount = 0 folds.between(start, stop, (from, to) => { let rangeStart = state.doc.lineAt(from).number let rangeStop = state.doc.lineAt(to).number foldedCount += rangeStop - rangeStart }) if (lineNo === cursorLine) { return blank; } else { return (Math.abs(cursorLine - lineNo) - foldedCount).toString().padStart(charLength, " "); } } // This shows the numbers in the gutter const showLineNumbers = relativeLineNumberGutter.of( lineNumbers({ formatNumber: relativeLineNumbers }) ); // This ensures the numbers update // when selection (cursorActivity) happens const lineNumbersUpdateListener = EditorView.updateListener.of( (viewUpdate: ViewUpdate) => { if (viewUpdate.selectionSet) { viewUpdate.view.dispatch({ effects: relativeLineNumberGutter.reconfigure( lineNumbers({ formatNumber: relativeLineNumbers }) ), }); } } ); export function lineNumbersRelative(): Extension { return [absoluteLineNumberGutter, showLineNumbers, lineNumbersUpdateListener]; } ================================================ FILE: flake.nix ================================================ { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; }; outputs = { self, nixpkgs }: let overlays = []; supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ]; forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f { pkgs = import nixpkgs { inherit overlays system; }; }); in { devShells = forEachSupportedSystem ({ pkgs }: { default = pkgs.mkShell { packages = with pkgs; [ nodejs nodePackages.typescript ]; }; }); }; } ================================================ FILE: main.ts ================================================ import { Plugin } from "obsidian"; import { lineNumbersRelative } from "./extension"; import { Extension } from "@codemirror/state"; export default class RelativeLineNumbers extends Plugin { private editorExtension: Extension[] = []; enabled: boolean; isLegacy() { return (this.app as any).vault.config?.legacyEditor; } async onload() { this.registerEditorExtension(this.editorExtension); // @ts-ignore const showLineNumber: Boolean = this.app.vault.getConfig("showLineNumber"); if (showLineNumber) { this.enable(); } this.setupConfigChangeListener(); this.addCommand({ id: "toggle-relative-line-numbers", name: "Toggle Relative Line Numbers", callback: () => { if (showLineNumber) { if (this.enabled) { this.disable(); } else { this.enable(); } } }, }); } onunload() { this.disable(); } enable() { this.enabled = true; if (this.isLegacy()) { this.legacyEnable(); } else { this.editorExtension.length = 0; this.editorExtension.push(lineNumbersRelative()); this.app.workspace.updateOptions(); } } disable() { this.enabled = false; if (this.isLegacy()) { this.legacyDisable(); } else { this.editorExtension.length = 0; this.app.workspace.updateOptions(); } } legacyEnable() { this.registerCodeMirror((cm) => { cm.on("cursorActivity", this.legacyRelativeLineNumbers); }); } legacyDisable() { this.app.workspace.iterateCodeMirrors((cm) => { cm.off("cursorActivity", this.legacyRelativeLineNumbers); cm.setOption( "lineNumberFormatter", // @ts-ignore CodeMirror.defaults["lineNumberFormatter"] ); }); } setupConfigChangeListener() { // @ts-ignore const configChangedEvent = this.app.vault.on("config-changed", () => { const showLineNumber: Boolean = // @ts-ignore this.app.vault.getConfig("showLineNumber"); if (showLineNumber && !this.enabled) { this.enable(); } else if (!showLineNumber && this.enabled) { this.disable(); } }); // @ts-ignore configChangedEvent.ctx = this; this.registerEvent(configChangedEvent); } legacyRelativeLineNumbers(cm: CodeMirror.Editor) { const current = cm.getCursor().line + 1; if (cm.state.curLineNum === current) { return; } cm.state.curLineNum = current; cm.setOption("lineNumberFormatter", (line: number) => { if (line === current) { return String(current); } return String(Math.abs(current - line)); }); } } ================================================ FILE: manifest.json ================================================ { "id": "obsidian-relative-line-numbers", "name": "Relative Line Numbers", "version": "3.0.0", "minAppVersion": "1.4.16", "description": "Enables relative line numbers in editor mode", "author": "Nadav Spiegelman", "authorUrl": "https://nadav.is", "isDesktopOnly": false } ================================================ FILE: package.json ================================================ { "name": "obsidian-relative-line-numbers", "version": "3.0.0", "description": "Enables relative line numbers in editor mode", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [], "author": "Nadav Spiegelman", "license": "MIT", "devDependencies": { "@codemirror/language": "^6.6.0", "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.2.0", "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "builtin-modules": "^3.2.0", "esbuild": "0.13.12", "obsidian": "1.4.11", "tslib": "2.3.1", "typescript": "^5.3.3" } } ================================================ FILE: styles.css ================================================ .relative-line-numbers-mono { font-family: monospace; white-space: pre; } .cm-lineNumbers { font-family: monospace; white-space: pre; min-width: 25px; /* prevent relative line numbers from shifting on files with ~10-20 lines */ } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "inlineSourceMap": true, "inlineSources": true, "module": "ESNext", "target": "es5", "allowJs": true, "noImplicitAny": true, "moduleResolution": "node", "importHelpers": true, "lib": [ "dom", "es5", "scripthost", "es2015" ] }, "include": [ "**/*.ts" ] } ================================================ FILE: version-bump.mjs ================================================ import { readFileSync, writeFileSync } from "fs"; const targetVersion = process.env.npm_package_version; // read minAppVersion from manifest.json and bump version to target version let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); const { minAppVersion } = manifest; manifest.version = targetVersion; writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); // update versions.json with target version and minAppVersion from manifest.json let versions = JSON.parse(readFileSync("versions.json", "utf8")); versions[targetVersion] = minAppVersion; writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); ================================================ FILE: versions.json ================================================ { "1.0.3": "0.10.0", "1.0.2": "0.10.0", "1.0.1": "0.10.0", "2.0.0": "0.13.14", "2.0.1": "0.13.14", "3.0.0": "1.4.16" }