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