master 0c2ee77325cc cached
59 files
120.9 KB
33.7k tokens
227 symbols
1 requests
Download .txt
Repository: dominiclet/obsidian-note-definitions
Branch: master
Commit: 0c2ee77325cc
Files: 59
Total size: 120.9 KB

Directory structure:
gitextract_c7dro_a6/

├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github/
│   └── workflows/
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .npmrc
├── .prettierignore
├── LICENSE
├── README.md
├── __mocks__/
│   ├── internals.ts
│   └── obsidian.ts
├── babel.config.js
├── docs/
│   └── grammar.md
├── esbuild.config.mjs
├── jest.config.js
├── manifest.json
├── package.json
├── src/
│   ├── core/
│   │   ├── atomic-def-parser.ts
│   │   ├── base-def-parser.ts
│   │   ├── consolidated-def-parser.ts
│   │   ├── def-file-manager.ts
│   │   ├── def-file-updater.ts
│   │   ├── file-parser.ts
│   │   ├── file-type.ts
│   │   ├── fm-builder.ts
│   │   └── model.ts
│   ├── editor/
│   │   ├── add-modal.ts
│   │   ├── common.ts
│   │   ├── decoration.ts
│   │   ├── def-file-registration.ts
│   │   ├── definition-popover.ts
│   │   ├── definition-search.ts
│   │   ├── edit-modal.ts
│   │   ├── frontmatter-suggest-modal.ts
│   │   ├── md-postprocessor.ts
│   │   ├── mobile/
│   │   │   └── definition-modal.ts
│   │   └── prefix-tree.ts
│   ├── globals.ts
│   ├── main.ts
│   ├── settings.ts
│   ├── tests/
│   │   ├── consolidated-def-parser.test.ts
│   │   ├── decorator.test.ts
│   │   ├── def-file-samples/
│   │   │   ├── case-sensitve-definitions-test.md
│   │   │   ├── consolidated-definitions-test.md
│   │   │   ├── consolidated-start-of-file-whitespace.md
│   │   │   ├── consolidated-trailing-delimiter.md
│   │   │   └── consolidated-trailing-whitespace.md
│   │   └── def-file-updater.test.ts
│   ├── types/
│   │   └── obsidian.d.ts
│   ├── ui/
│   │   └── file-explorer.ts
│   └── util/
│       ├── editor.ts
│       ├── log.ts
│       └── retry.ts
├── styles.css
├── tsconfig.json
├── version-bump.mjs
└── versions.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# top-most EditorConfig file
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4


================================================
FILE: .eslintignore
================================================
node_modules/

main.js


================================================
FILE: .eslintrc
================================================
{
    "root": true,
    "parser": "@typescript-eslint/parser",
    "env": { "node": true },
    "plugins": [
      "@typescript-eslint"
    ],
    "extends": [
      "eslint:recommended",
      "plugin:@typescript-eslint/eslint-recommended",
      "plugin:@typescript-eslint/recommended"
    ], 
    "parserOptions": {
        "sourceType": "module"
    },
    "rules": {
      "no-unused-vars": "off",
      "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
      "@typescript-eslint/ban-ts-comment": "off",
      "no-prototype-builtins": "off",
      "@typescript-eslint/no-empty-function": "off"
    } 
  }

================================================
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: .github/workflows/test.yml
================================================
name: Test

on:
  pull_request:
    branches:
      - master

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v5
      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
      - run: npm ci
      - run: npm run build --if-present
      - run: npm test


================================================
FILE: .gitignore
================================================
# vscode
.vscode 

# Intellij
*.iml
.idea

# npm
node_modules

# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js

# Exclude sourcemaps
*.map

# obsidian
data.json

# Exclude macOS Finder (System Explorer) View States
.DS_Store


================================================
FILE: .husky/pre-commit
================================================
npm test
npx lint-staged


================================================
FILE: .npmrc
================================================
tag-version-prefix=""

================================================
FILE: .prettierignore
================================================
# Ignore docs
**/*.md

# JS files are generated, no need to format
*.js
*.mjs

# Ignore github files
.github/


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2024 dominiclet

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Obsidian Note Definitions

A personal dictionary that can be easily looked-up from within your notes.

![dropdown](./img/def-dropdown.png)

## Basic usage

1. Create a folder, right-click on the folder in your file explorer, and select `Set definition folder`. This registers the folder as your definition folder.
2. Within the folder, create definition files (with any name of your choice).
3. Add a definition using the `Add definition` command. This will display a pop-up modal, where you can input your definition.
4. Once a definition is added, the word/phrase should be underlined in your notes. You may preview the definition of the word by hovering over the underlined word/phrase with the mouse, or triggering the `Preview definition` command when your cursor is on the word/phrase.

### Editor menu

Options available:
- Go to definition (jump to definition of word/phrase)
- Add definition (the text that you want to define must be highlighted for this to be available)
- Edit definition (right-click on an underlined definition)

### Commands

You may want to assign hotkeys to the commands available for easy access:
- Preview definition (show definition popover)
- Go to definition (jump to definition of word/phrase)
- Add definition
- Add definition context (see [Definition context](#definition-context))
- Register consolidated definition file
- Register atomic definition file
- Refresh definitions

## How it works

**Note Definitions** does not maintain any hidden metadata files for your definitions. 
All definitions are placed in your vault and form part of your notes.
You will notice that added definitions will create entries within your selected definition file. 
You may edit these entries freely to add/edit your definitions, but if you do so, make sure to adhere strictly to the definition rules below.
**It is recommended that you read through the definition rules first before manually editing the definition files.**

### Definition rules

Currently, there are two types of definition files: `consolidated` and `atomic`.
The type of definition file is specified in the `def-type` frontmatter (or property) of a file.
For all definition files you create, the `def-type` frontmatter should be set to either 'consolidated' or 'atomic'.
For compatibility reasons, a file is treated to be `consolidated` if the `def-type` frontmatter is not specified (but this is not guaranteed to remain the same in subsequent releases, so always specify the frontmatter when creating a new definition file). 
For convenience, use the commands provided to add the `def-type` frontmatter.

#### Consolidated definition file

A `consolidated` file type refers to a file that can contain many definitions.
Register a definition file by specifying the `def-type: consolidated` frontmatter, or using the `Register consolidated definition file` command when the file is active.

A `consolidated` definition file is parsed according to the following rules:

1. A definition block consists of a **phrase (1 or more words), an alias (optional) and a definition**. They must be provided **strictly** in that order.
2. A phrase is denoted with a line in the following format `# <PHRASE>`. This is rendered as a markdown header in Obsidian.
3. An **optional** comma-separated line of alias(es) is expected after a phrase. This must be a line surrounded by asterisks, eg. `*alias*`. *This is rendered as italics in Obsidian*.
4. A line that occurs after a registered **phrase** and is not an alias is deemed to be a definition. Definitions can be multi-line. All subsequent lines are definitions until the definition block divider is encountered. You may write markdown here, which will be formatted similar to Obsidian's markdown formatting.
5. A line with nothing but three hyphens `---` is used as a divider to separate definition blocks. This is rendered as a delimiting line in Obsidian. (This divider can be configured in the settings to recognise three underscores `___` as well)

Example definition file:

> # Word1
> 
> *alias of word1*
> 
> Definition of word1.
> This definition can span several lines.
> It will end when the divider is reached.
> 
> ---
> 
> # Word2
>
> Notice that there is no alias here as it is optional.
> The last word in the file does not need to have a divider, although it is still valid to have one.
> 
> ---
> 
> # Phrase with multiple words
> 
> You can also define a phrase containing multiple words. 
>
> ---
>
> # Markdown support
> 
> Markdown is supported so you can do things like including *italics* or **bold** words.

For a more formal definition of the grammar of the consolidated definition file, you may refer to [this document](docs/grammar.md). 

#### Atomic definition file

An `atomic` definition file refers to a file that contains only one definition.
Register an atomic definition file by specifying the `def-type: atomic` frontmatter, or using the `Register atomic definition file` command when the file is active.

An `atomic` definition file is parsed according to the following rules:
1. The name of the file is the word/phrase defined
2. Aliases are specified in the `aliases` frontmatter as a list. In source, it should look something like this:
```
---
aliases:
  - alias1
  - alias2
---
```
3. The contents of the file (excluding the frontmatter) form the definition

## Definition context
> _TLDR:_ "Context" is synonymous with a definition file. By specifying a context, you specify that you want to use specific definition file(s) to source your definitions for the current note.

Definition context refers to the repository of definitions that are available for the currently active note.
By default, all notes have no context (you can think of this as being globally-scoped).
This means that your newly-created notes will always have access to the combination of all definitions defined in your definition files.

This behaviour can be overridden by specifying the "context" of your note.
Each definition file that you have is taken to be a separate context (hence your definitions should be structured accordingly).
Once context(s) are declared for a note, it will only retrieve definitions from the specified contexts.
You can think of this as having a local scope for the note.
The note now sees only a limited subset of all your definitions.

### Usage

To easily add context to your note:
1. Use the `Add definition context` command
2. Search and select your desired context

You can do this multiple times to add multiple contexts.

### How it works

`Add definition context` adds to the _properties_ of your note.
Specifically, it adds to the `def-context` property, which is a `List` type containing a list of file paths corresponding to the selected definition files.
In source, it will look something like this:
```
---
def-context:
	- definitions/def1.md
	- definitions/def2.md
---
```

You can edit your properties directly, although for convenience, it is recommended to use the `Add definition context` command to add contexts as it is easy to get file paths wrong.

### Removing contexts

To remove contexts, simply remove the file path from the `def-context` property.
Or if you want to remove all contexts, you can delete the `def-context` property altogether.

## Refreshing definitions

Whenever you find that the plugin is not detecting certain definitions or definition files, run the `Refresh definitions` command to manually get the plugin to read your definition files.

## Feedback

I welcome any feedback on how to improve this tool.
Do let me know by opening a Github issue if you find any bugs, or have any ideas for features or improvements.

## Contributing

If you're a programmer and would like to see certain features implemented, I welcome and would be grateful for contributions. If you are interested, please do let me know in the issue thread.


================================================
FILE: __mocks__/internals.ts
================================================
export class DefManager {
	loadUpdatedFiles() {}
}


================================================
FILE: __mocks__/obsidian.ts
================================================
export class App {
	vault: Vault;
	metadataCache: MetadataCache;

	constructor() {
		this.vault = new Vault();
		this.metadataCache = new MetadataCache();
	}
}

export class TFile {
	basename: string;
	extension: string;
	
	// Ignore other properties
}

export class PluginSettingTab {}

export class Vault {
	modify(file: TFile, data: string) {}
	read(file: TFile): Promise<string> {
		return Promise.resolve("");
	}
}

export class MetadataCache {
	getFileCache(file: TFile) {
		return null;
	}
}

export class Notice {}


================================================
FILE: babel.config.js
================================================
module.exports = {presets: ['@babel/preset-env', "@babel/preset-typescript"]}


================================================
FILE: docs/grammar.md
================================================
# Definition File Grammar

This file documents the formal grammar defined for the definition files. It should give you some insight into how your definition files are parsed, if the README documentation is insufficient.
If you notice that the behaviour of the parser departs from the documented grammar here, do let me know by raising an issue.

The following is written in extended Backus-Naur form.

## Consolidated definition file grammar

```text
doc = { def-block, [ delimiter, { "\n" }] }, eof;
def-block = header, "\n", [ alias, { char }, "\n" ], def;
header = "#", " ", { char };
alias = { "\n" }, "*", [{ { char }, "," }], { char }, "*";
def = { char };
char = any char;
delimiter = "\n", { " " }, "-", "-", "-", { " " }, "\n";
```

The only terminal is an end-of-file after all def-blocks. If a delimeter is given, then another def-block is expected.
So a valid file should not provide a trailing delimiter at the end of the file.


================================================
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");

const context = await esbuild.context({
	banner: {
		js: banner,
	},
	entryPoints: ["src/main.ts"],
	bundle: true,
	external: [
		"obsidian",
		"electron",
		"@codemirror/autocomplete",
		"@codemirror/collab",
		"@codemirror/commands",
		"@codemirror/language",
		"@codemirror/lint",
		"@codemirror/search",
		"@codemirror/state",
		"@codemirror/view",
		"@lezer/common",
		"@lezer/highlight",
		"@lezer/lr",
		...builtins],
	format: "cjs",
	target: "es2018",
	logLevel: "info",
	sourcemap: prod ? false : "inline",
	treeShaking: true,
	outfile: "main.js",
});

if (prod) {
	await context.rebuild();
	process.exit(0);
} else {
	await context.watch();
}


================================================
FILE: jest.config.js
================================================
module.exports = {
    preset: "ts-jest",
    testEnvironment: "node",
	transform: {
		'^.+\\.ts$': 'ts-jest',
	},
	moduleDirectories: ["node_modules", "<rootDir>"],
	moduleFileExtensions: ["ts", "js"],
	roots: ["<rootDir>"],
	modulePaths: ["<rootDir>"],
};


================================================
FILE: manifest.json
================================================
{
	"id": "note-definitions",
	"name": "Note Definitions",
	"version": "0.29.1",
	"minAppVersion": "1.5.12",
	"description": "Personal dictionary for your notes",
	"author": "Dominic Let",
	"isDesktopOnly": false
}


================================================
FILE: package.json
================================================
{
	"name": "obsidian-note-definitions",
	"version": "0.29.1",
	"description": "Personal dictionary for your notes",
	"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",
		"test": "jest --config ./jest.config.js",
		"prepare": "husky"
	},
	"keywords": [],
	"author": "",
	"license": "MIT",
	"devDependencies": {
		"@types/jest": "^29.5.14",
		"@types/node": "^16.11.6",
		"@typescript-eslint/eslint-plugin": "5.29.0",
		"@typescript-eslint/parser": "5.29.0",
		"builtin-modules": "3.3.0",
		"esbuild": "0.17.3",
		"husky": "^9.1.7",
		"jest": "^29.7.0",
		"lint-staged": "^16.2.7",
		"obsidian": "latest",
		"prettier": "3.8.1",
		"ts-jest": "^29.2.5",
		"tslib": "2.4.0",
		"typescript": "4.7.4"
	},
	"dependencies": {
		"pluralize": "^8.0.0"
	},
	"lint-staged": {
		"*.{ts,tsx,css,scss,json}": "prettier --write"
	}
}


================================================
FILE: src/core/atomic-def-parser.ts
================================================
import { BaseDefParser } from "./base-def-parser";
import { App, TFile } from "obsidian";
import { Definition } from "./model";
import { DefFileType } from "./file-type";

export class AtomicDefParser extends BaseDefParser {
	app: App;
	file: TFile;

	constructor(app: App, file: TFile) {
		super();

		this.app = app;
		this.file = file;
	}

	async parseFile(fileContent?: string): Promise<Definition[]> {
		if (!fileContent) {
			fileContent = await this.app.vault.cachedRead(this.file);
		}

		const fileMetadata = this.app.metadataCache.getFileCache(this.file);
		let aliases = [];
		const fmData = fileMetadata?.frontmatter;
		if (fmData) {
			const fmAlias = fmData["aliases"];
			if (Array.isArray(fmAlias)) {
				aliases = fmAlias;
			}
		}
		const fmPos = fileMetadata?.frontmatterPosition;
		if (fmPos) {
			fileContent = fileContent.slice(fmPos.end.offset + 1);
		}

		let key = this.parseSettings.enableCaseSensitive ? this.file.basename : this.file.basename.toLowerCase();
		
		aliases = aliases.concat(
			this.calculatePlurals([key].concat(aliases)),
		);

		const def = {
			key: key,
			word: this.file.basename,
			aliases: aliases,
			definition: fileContent,
			file: this.file,
			linkText: `${this.file.path}`,
			fileType: DefFileType.Atomic,
		};
		return [def];
	}
}


================================================
FILE: src/core/base-def-parser.ts
================================================
import { DefFileParseConfig, getSettings } from "src/settings";

var pluralize = require("pluralize");

export class BaseDefParser {
	parseSettings: DefFileParseConfig;

	constructor(parseSettings?: DefFileParseConfig) {
		this.parseSettings = parseSettings
			? parseSettings
			: this.getParseSettings();
	}

	calculatePlurals(aliases: string[]) {
		let plurals: string[] = [];

		if (this.parseSettings.autoPlurals) {
			aliases.forEach((alias) => {
				let pl = pluralize(alias);
				if (pl !== alias) {
					plurals.push(pl);
				}
			});
		}

		return plurals;
	}

	getParseSettings(): DefFileParseConfig {
		return getSettings().defFileParseConfig;
	}
}


================================================
FILE: src/core/consolidated-def-parser.ts
================================================
import { App, TFile } from "obsidian";
import { BaseDefParser } from "src/core/base-def-parser";
import { DefFileParseConfig } from "src/settings";
import { DefFileType } from "./file-type";
import { Definition, FilePosition } from "./model";

interface DocAST {
	blocks: DefblockAST[];
}

interface DefblockAST {
	header: string;
	aliases: string[];
	body: string;
	position: FilePosition;
}

const EOF = "";

export class ConsolidatedDefParser extends BaseDefParser {
	app: App;
	file: TFile;
	parseSettings: DefFileParseConfig;

	fileContent: string;
	cursor: number;
	currLine: number;

	constructor(app: App, file: TFile, parseSettings?: DefFileParseConfig) {
		super(parseSettings);

		this.app = app;
		this.file = file;

		this.parseSettings = parseSettings
			? parseSettings
			: this.getParseSettings();

		this.fileContent = "";
		this.currLine = 0;
	}

	async parseFile(fileContent?: string): Promise<Definition[]> {
		if (fileContent === "") {
			return [];
		}
		if (!fileContent) {
			fileContent = await this.app.vault.cachedRead(this.file);
		}

		// Ignore frontmatter (properties)
		const fileMetadata = this.app.metadataCache.getFileCache(this.file);
		const fmPos = fileMetadata?.frontmatterPosition;
		if (fmPos) {
			fileContent = fileContent.slice(fmPos.end.offset + 1);
		}
		return this.directParseFile(fileContent);
	}

	// Parse from string, no dependency on App
	// For ease of testing
	directParseFile(fileContent: string): Definition[] {
		this.fileContent = fileContent;
		this.currLine = 0;
		this.cursor = 0;
		const doc = this.parseDoc();
		return doc.blocks.map((blk) => this.defBlockToDefinition(blk));
	}

	private parseDoc(): DocAST {
		const blocks = [];
		while (this.cursor < this.fileContent.length) {
			// Ignore leading newlines (and whitespace)
			let c;
			do {
				c = this.consumeChar();
			} while (/\s/.test(c));

			// If EOF encountered, just return
			if (c === EOF) {
				return {
					blocks,
				};
			}
			// otherwise return character to def block
			this.spitChar();

			blocks.push(this.parseDefBlock());
		}
		return {
			blocks,
		};
	}

	private parseDefBlock(): DefblockAST {
		const posStart = this.currLine;
		let header = this.parseHeader();
		let aliases = this.parseAliases();
		let def = this.parseDef();
		const posEnd = this.currLine - 1;
		return {
			header,
			aliases,
			body: def,
			position: {
				from: posStart,
				to: posEnd,
			},
		};
	}

	private parseHeader(): string {
		const h = this.consumeChar();

		if (h != "#") {
			throw new Error(
				`Parse Header for ${this.file.path} (at line ${this.currLine}): Unexpected character '${h}', expected '#'`,
			);
		}
		let s = this.consumeChar();
		if (s != " ") {
			throw new Error(
				`Parse Header for ${this.file.path} (at line ${this.currLine}): Unexpected character '${s}', expected SPACE`,
			);
		}

		let header = [];
		while (true) {
			let c = this.consumeChar();
			if (c == "\n") {
				break;
			}
			header.push(c);
		}
		return header.join("");
	}

	private parseAliases(): string[] {
		let asterisk;
		do {
			asterisk = this.consumeChar();
		} while (asterisk == "\n");

		if (asterisk != "*") {
			// aliases optional, so backtrack
			this.spitChar();
			return [];
		}

		// Consume until reach ASTERISK
		let aliasStart = this.cursor;
		let aliasEnd = aliasStart;
		while (true) {
			let c = this.consumeChar();
			if (c == "\n") {
				// If we encounter a newline before a '*',
				// then determine that there is no alias declaration
				this.cursor = aliasStart - 1;
				return [];
			}
			if (c == "*") {
				break;
			}
			aliasEnd++;
		}
		let aliasStr = this.fileContent.slice(aliasStart, aliasEnd);
		const aliases = aliasStr.split(/[,|]/);

		// Continue consuming until newline (but all chars after the closing ASTERISK are ignored)
		while (this.consumeChar() != "\n") {}

		return aliases.map((alias) => alias.trim());
	}

	private parseDef(): string {
		let defStr = "";

		while (true) {
			let c = this.consumeChar();
			if (c === EOF) {
				// On EOF, treat all preceding chars as definition
				return defStr;
			}
			defStr += c;
			if (defStr.length >= 5) {
				if (this.checkDelimiter(defStr.slice(defStr.length - 5))) {
					return defStr.slice(0, defStr.length - 5);
				}
			}
		}
	}

	private checkDelimiter(d: string) {
		const r = /\n *((---)|(___)) *\n/;
		return r.test(d);
	}

	// For backtracking, used for optional grammars rules
	private spitChar(count?: number) {
		if (!count) {
			count = 1;
		}
		for (let i = 0; i < count; i++) {
			this.cursor--;
		}
	}

	private consumeChar(): string {
		if (this.cursor >= this.fileContent.length) {
			return EOF;
		}
		const c = this.fileContent[this.cursor++];
		if (c === "\n") {
			this.currLine++;
		}
		return c;
	}

	private headerToKey(key: string): string {
		return this.parseSettings.enableCaseSensitive ? key : key.toLowerCase();
	}

    private defBlockToDefinition(blk: DefblockAST): Definition {
        return {
            key: this.headerToKey(blk.header),
            word: blk.header,
            aliases: blk.aliases.concat(
				this.calculatePlurals([blk.header].concat(blk.aliases)),
			),
            definition: blk.body.trim(),
            file: this.file,
			linkText: `${this.file.path}${blk.header ? '#' + blk.header : ''}`,
			fileType: DefFileType.Consolidated,
			position: {
				from: blk.position.from,
				to: blk.position.to,
			},
		};
	}
}


================================================
FILE: src/core/def-file-manager.ts
================================================
import { App, TFile, TFolder } from "obsidian";
import { PTreeNode } from "src/editor/prefix-tree";
import { DEFAULT_DEF_FOLDER, VALID_DEFINITION_FILE_TYPES } from "src/settings";
import { normaliseWord } from "src/util/editor";
import { logDebug, logWarn } from "src/util/log";
import { useRetry } from "src/util/retry";
import { FileParser } from "./file-parser";
import { DefFileType } from "./file-type";
import { Definition } from "./model";
import { getSettings } from "src/settings";

let defFileManager: DefManager;

export const DEF_CTX_FM_KEY = "def-context";

export class DefManager {
	app: App;
	globalDefs: DefinitionRepo;
	globalDefFolders: Map<string, TFolder>;
	globalDefFiles: Map<string, TFile>;
	globalPrefixTree: PTreeNode;
	lastUpdate: number;

	markedDirty: TFile[];

	consolidatedDefFiles: Map<string, TFile>;

	activeFile: TFile | null;
	localPrefixTree: PTreeNode;
	shouldUseLocal: boolean;

	localDefs: DefinitionRepo;

	constructor(app: App) {
		this.app = app;
		this.globalDefs = new DefinitionRepo();
		this.globalDefFiles = new Map<string, TFile>();
		this.globalDefFolders = new Map<string, TFolder>();
		this.globalPrefixTree = new PTreeNode();
		this.consolidatedDefFiles = new Map<string, TFile>();
		this.localDefs = new DefinitionRepo();

		this.resetLocalConfigs();
		this.lastUpdate = 0;
		this.markedDirty = [];

		activeWindow.NoteDefinition.definitions.global = this.globalDefs;

		this.loadDefinitions();
	}

	addDefFile(file: TFile) {
		this.globalDefFiles.set(file.path, file);
	}

	// Get the appropriate prefix tree to use for current active file
	getPrefixTree() {
		if (this.shouldUseLocal) {
			return this.localPrefixTree;
		}
		return this.globalPrefixTree;
	}

	// Updates active file and rebuilds local prefix tree if necessary
	updateActiveFile() {
		this.activeFile = this.app.workspace.getActiveFile();
		this.resetLocalConfigs();

		if (this.activeFile) {
			const metadataCache = this.app.metadataCache.getFileCache(
				this.activeFile,
			);
			if (!metadataCache) {
				return;
			}
			const paths = metadataCache.frontmatter?.[DEF_CTX_FM_KEY];
			if (!paths) {
				// No def-source specified
				return;
			}
			if (!Array.isArray(paths)) {
				logWarn(
					`Unrecognised type for '${DEF_CTX_FM_KEY}' frontmatter`,
				);
				return;
			}
			const flattenedPaths = this.flattenPathList(paths);
			this.buildLocalPrefixTree(flattenedPaths);
			this.buildLocalDefRepo(flattenedPaths);
			this.shouldUseLocal = true;
		}
	}

	// For manually updating definition sources, as metadata cache may not be the latest updated version
	updateDefSources(defSource: string[]) {
		this.resetLocalConfigs();

		if (!defSource || defSource.length === 0) {
			return;
		}
		this.buildLocalPrefixTree(defSource);
		this.buildLocalDefRepo(defSource);
		this.shouldUseLocal = true;
	}

	markDirty(file: TFile) {
		this.markedDirty.push(file);
	}

	private flattenPathList(paths: string[]): string[] {
		const filePaths: string[] = [];
		paths.forEach((path) => {
			if (this.isFolderPath(path)) {
				filePaths.push(...this.flattenFolder(path));
			} else {
				filePaths.push(path);
			}
		});
		return filePaths;
	}

	// Given a folder path, return an array of file paths
	private flattenFolder(path: string): string[] {
		if (path.endsWith("/")) {
			path = path.slice(0, path.length - 1);
		}
		const folder = this.app.vault.getFolderByPath(path);
		if (!folder) {
			return [];
		}
		const childrenFiles = this.getChildrenFiles(folder);
		return childrenFiles.map((file) => file.path);
	}

	private getChildrenFiles(folder: TFolder): TFile[] {
		const files: TFile[] = [];
		folder.children.forEach((abstractFile) => {
			if (abstractFile instanceof TFolder) {
				files.push(...this.getChildrenFiles(abstractFile));
			} else if (abstractFile instanceof TFile) {
				files.push(abstractFile);
			}
		});
		return files;
	}

	private isFolderPath(path: string): boolean {
		return path.endsWith("/");
	}

	// Expects an array of file paths (not directories)
	private buildLocalPrefixTree(filePaths: string[]) {
		const root = new PTreeNode();
		filePaths.forEach((filePath) => {
			const defMap = this.globalDefs.getMapForFile(filePath);
			if (!defMap) {
				logWarn(`Unrecognised file path '${filePath}'`);
				return;
			}
			[...defMap.keys()].forEach((key) => {
				root.add(key, 0);
			});
		});
		this.localPrefixTree = root;
	}

	// Expects an array of file paths (not directories)
	private buildLocalDefRepo(filePaths: string[]) {
		filePaths.forEach((filePath) => {
			const defMap = this.globalDefs.getMapForFile(filePath);
			if (defMap) {
				this.localDefs.fileDefMap.set(filePath, defMap);
			}
		});
	}

	isDefFile(file: TFile): boolean {
		return (
			file.path.startsWith(this.getGlobalDefFolder()) &&
			VALID_DEFINITION_FILE_TYPES.some((ext) => file.path.endsWith(ext))
		);
	}

	reset() {
		this.globalPrefixTree = new PTreeNode();
		this.globalDefs.clear();
		this.globalDefFiles = new Map<string, TFile>();
	}

	// Load all definitions from registered def folder
	// This will recurse through the def folder, parsing all definition files
	// Expensive operation so use sparingly
	loadDefinitions() {
		this.reset();
		this.loadGlobals().then(this.updateActiveFile.bind(this));
	}

	private getDefRepo() {
		return this.shouldUseLocal ? this.localDefs : this.globalDefs;
	}

	get(key: string) {
		return this.getDefRepo().get(normaliseWord(key));
	}

	set(def: Definition) {
		this.globalDefs.set(def);
	}

	getDefFiles(): TFile[] {
		return [...this.globalDefFiles.values()];
	}

	getConsolidatedDefFiles(): TFile[] {
		return [...this.consolidatedDefFiles.values()];
	}

	getDefFolders(): TFolder[] {
		return [...this.globalDefFolders.values()];
	}

	async loadUpdatedFiles() {
		const definitions: Definition[] = [];
		const dirtyFiles: string[] = [];

		const files = [...this.globalDefFiles.values(), ...this.markedDirty];

		for (let file of files) {
			if (file.stat.mtime > this.lastUpdate) {
				logDebug(
					`File ${file.path} was updated, reloading definitions...`,
				);
				dirtyFiles.push(file.path);
				const defs = await this.parseFile(file);
				definitions.push(...defs);
			}
		}

		dirtyFiles.forEach((file) => {
			this.globalDefs.clearForFile(file);
		});

		if (definitions.length > 0) {
			definitions.forEach((def) => {
				this.globalDefs.set(def);
			});
		}

		this.markedDirty = [];
		this.buildPrefixTree();
		this.lastUpdate = Date.now();
	}

	// Global configs should always be used by default
	private resetLocalConfigs() {
		this.localPrefixTree = new PTreeNode();
		this.shouldUseLocal = false;
		this.localDefs.clear();
	}

	private async loadGlobals() {
		const retry = useRetry();
		let globalFolder: TFolder | null = null;
		// Retry is needed here as getFolderByPath may return null when being called on app startup
		await retry.exec(() => {
			globalFolder = this.app.vault.getFolderByPath(
				this.getGlobalDefFolder(),
			);
			if (!globalFolder) {
				retry.setShouldRetry();
			}
		});

		if (!globalFolder) {
			logWarn(
				"Global definition folder not found, unable to load global definitions",
			);
			return;
		}

		// Recursively load files within the global definition folder
		const definitions = await this.parseFolder(globalFolder);
		definitions.forEach((def) => {
			this.globalDefs.set(def);
		});

		this.buildPrefixTree();
		this.lastUpdate = Date.now();
	}

	private async buildPrefixTree() {
		const root = new PTreeNode();
		this.globalDefs.getAllKeys().forEach((key) => {
			root.add(key, 0);
		});
		this.globalPrefixTree = root;
	}

	private async parseFolder(folder: TFolder): Promise<Definition[]> {
		this.globalDefFolders.set(folder.path, folder);
		const definitions: Definition[] = [];
		for (let f of folder.children) {
			if (f instanceof TFolder) {
				let defs = await this.parseFolder(f);
				definitions.push(...defs);
			} else if (f instanceof TFile && this.isDefFile(f)) {
				let defs = await this.parseFile(f);
				definitions.push(...defs);
			}
		}
		return definitions;
	}

	private async parseFile(file: TFile): Promise<Definition[]> {
		this.globalDefFiles.set(file.path, file);
		let parser = new FileParser(this.app, file);
		const def = await parser.parseFile();
		if (parser.defFileType === DefFileType.Consolidated) {
			this.consolidatedDefFiles.set(file.path, file);
		}
		return def;
	}

	// Walk the definition directory to find definition files and folders
	getDefFilesAndFolders(): [TFolder[], TFile[]] {
		const parentDefFolder = this.app.vault.getFolderByPath(
			this.getGlobalDefFolder(),
		);
		if (!parentDefFolder) {
			logWarn("Failed to get parent def folder");
			return [[], []];
		}
		return this.walkFolder(parentDefFolder);
	}

	private walkFolder(folder: TFolder): [TFolder[], TFile[]] {
		this.globalDefFolders.set(folder.path, folder);
		const folders = [folder];
		const files = [];
		for (let f of folder.children) {
			if (f instanceof TFolder) {
				const [childFolders, childFiles] = this.walkFolder(f);
				folders.push(...childFolders);
				files.push(...childFiles);
			} else if (f instanceof TFile && this.isDefFile(f)) {
				this.globalDefFiles.set(f.path, f);
				files.push(f);
			}
		}
		return [folders, files];
	}

	getGlobalDefFolder() {
		return window.NoteDefinition.settings.defFolder || DEFAULT_DEF_FOLDER;
	}
}

export class DefinitionRepo {
	// file name -> {definition-key -> definition}
	fileDefMap: Map<string, Map<string, Definition>>;

	constructor() {
		this.fileDefMap = new Map<string, Map<string, Definition>>();
	}

	getMapForFile(filePath: string) {
		return this.fileDefMap.get(filePath);
	}

	get(key: string) {
		for (let [_, defMap] of this.fileDefMap) {
			const def = defMap.get(key);
			if (def) {
				return def;
			}
		}
	}

	getAllKeys(): string[] {
		const keys: string[] = [];
		this.fileDefMap.forEach((defMap, _) => {
			keys.push(...defMap.keys());
		});
		return keys;
	}

	set(def: Definition) {
		let defMap = this.fileDefMap.get(def.file.path);
		if (!defMap) {
			defMap = new Map<string, Definition>();
			this.fileDefMap.set(def.file.path, defMap);
		}
		// Prefer the first encounter over subsequent collisions
		if (defMap.has(def.key)) {
			return;
		}
		defMap.set(def.key, def);

		if (def.aliases.length > 0) {
			def.aliases.forEach((alias) => {
				if (defMap && getSettings().defFileParseConfig.enableCaseSensitive) {
					defMap.set(alias, def);
				} else if (defMap) {
					defMap.set(alias.toLowerCase(), def);
				}
			});
		}
	}

	clearForFile(filePath: string) {
		const defMap = this.fileDefMap.get(filePath);
		if (defMap) {
			defMap.clear();
		}
	}

	clear() {
		this.fileDefMap.clear();
	}
}

export function initDefFileManager(app: App): DefManager {
	defFileManager = new DefManager(app);
	return defFileManager;
}

export function getDefFileManager(): DefManager {
	return defFileManager;
}


================================================
FILE: src/core/def-file-updater.ts
================================================
import { App, Notice } from "obsidian";
import { getSettings } from "src/settings";
import { logError, logWarn } from "src/util/log";
import { getDefFileManager } from "./def-file-manager";
import { FileParser } from "./file-parser";
import { DefFileType } from "./file-type";
import { FrontmatterBuilder } from "./fm-builder";
import { Definition } from "./model";

export class DefFileUpdater {
	app: App;

	constructor(app: App) {
		this.app = app;
	}

	async updateDefinition(def: Definition) {
		// Ensure that key is case-insensitive
		def.key = def.key.toLowerCase();
		def.definition = def.definition.trim();

		if (def.fileType === DefFileType.Atomic) {
			await this.updateAtomicDefFile(def);
		} else if (def.fileType === DefFileType.Consolidated) {
			await this.updateConsolidatedDefFile(def);
		} else {
			return;
		}
		await getDefFileManager().loadUpdatedFiles();
		new Notice("Definition successfully modified");
	}

	private async updateAtomicDefFile(def: Definition) {
		await this.app.vault.modify(def.file, def.definition);
	}

	private async updateConsolidatedDefFile(def: Definition) {
		const file = def.file;
		const fileContent = await this.app.vault.read(file);

		const fileParser = new FileParser(this.app, file);
		const defs = await fileParser.parseFile(fileContent);

		const fileDef = defs.find((fileDef) => fileDef.key === def.key);
		if (!fileDef) {
			logError("File definition not found, cannot edit");
			return;
		}
		if (!fileDef.position) {
			logError("Position not set, cannot edit");
			return;
		}

		// Replace definition and aliases
		fileDef.definition = def.definition;
		fileDef.aliases = def.aliases;

		// account for frontmatter
		const fileMetadata = this.app.metadataCache.getFileCache(file);
		const fmPos = fileMetadata?.frontmatterPosition;
		let fmContent: string = "";
		if (fmPos) {
			fmContent = fileContent.slice(0, fmPos.end.offset + 1);
		}

		const newContent = this.generateConsDefFile(defs);

		await this.app.vault.modify(file, fmContent + newContent);
	}

	async addDefinition(def: Partial<Definition>, folder?: string) {
		def.word = def.word?.trim();
		def.definition = def.definition?.trim();
		if (!def.fileType) {
			logError("File type missing");
			return;
		}
		if (def.fileType === DefFileType.Consolidated) {
			await this.addConsolidatedFileDefinition(def);
		} else if (def.fileType === DefFileType.Atomic) {
			await this.addAtomicFileDefinition(def, folder);
		}
		await getDefFileManager().loadUpdatedFiles();
		new Notice("Definition succesfully added");
	}

	private async addAtomicFileDefinition(
		def: Partial<Definition>,
		folder?: string,
	) {
		if (!folder) {
			logError("Folder missing for atomic file add");
			return;
		}
		if (!def.definition) {
			logWarn("No definition given");
			return;
		}
		const fmBuilder = new FrontmatterBuilder();
		fmBuilder.add("def-type", "atomic");
		if (def.aliases) {
			const aliases: string[] = [];
			def.aliases.forEach((alias) => {
				aliases.push(`- ${alias}`);
			});
			fmBuilder.add("aliases", "\n" + aliases.join("\n"));
		}
		const fm = fmBuilder.finish();
		const file = await this.app.vault.create(
			`${folder}/${def.word}.md`,
			fm + def.definition,
		);

		getDefFileManager().addDefFile(file);
		getDefFileManager().markDirty(file);
	}

	private async addConsolidatedFileDefinition(def: Partial<Definition>) {
		const file = def.file;
		if (!file) {
			logError("Add definition failed, no file given");
			return;
		}
		const fileContent = await this.app.vault.read(file);
		const fileParser = new FileParser(this.app, file);
		const defs = await fileParser.parseFile(fileContent);

		// @ts-ignore: This is fine as long as word, alias (optional) and definition are present
		// Nothing else is used
		defs.push(def);

		// account for frontmatter
		const fileMetadata = this.app.metadataCache.getFileCache(file);
		const fmPos = fileMetadata?.frontmatterPosition;
		let fmContent: string = "";
		if (fmPos) {
			fmContent = fileContent.slice(0, fmPos.end.offset + 1);
		}

		const newContent = this.generateConsDefFile(defs);

		await this.app.vault.modify(file, fmContent + newContent);
	}

	private addSeparator(lines: string[]) {
		const dividerSettings = getSettings().defFileParseConfig.divider;
		let sepChoice = dividerSettings.underscore ? "___" : "---";
		lines.push("", sepChoice, "");
	}

	private constructLinesFromDef(def: Partial<Definition>): string[] {
		const lines = [`# ${def.word}`];
		if (def.aliases && def.aliases.length > 0) {
			const aliasStr = `*${def.aliases.join(", ")}*`;
			lines.push("", aliasStr);
		}
		const trimmedDef = def.definition
			? def.definition.replace(/\s+$/g, "")
			: "";
		lines.push("", trimmedDef);
		return lines;
	}

	// Given an array of definitions, generate the contents of a consolidated definition file
	// Remember that this does not consider the frontmatter of a file
	private generateConsDefFile(defs: Definition[]): string {
		const lines: string[] = [];
		defs.forEach((def, idx) => {
			const defLines = this.constructLinesFromDef(def);
			lines.push(...defLines);
			if (idx !== defs.length - 1) {
				this.addSeparator(lines);
			}
		});
		return lines.join("\n");
	}
}


================================================
FILE: src/core/file-parser.ts
================================================
import { App, CachedMetadata, TFile } from "obsidian";
import { getSettings } from "src/settings";
import { useRetry } from "src/util/retry";
import { AtomicDefParser } from "./atomic-def-parser";
import { ConsolidatedDefParser } from "./consolidated-def-parser";
import { DefFileType } from "./file-type";
import { Definition } from "./model";

export const DEF_TYPE_FM = "def-type";

export class FileParser {
	app: App;
	file: TFile;
	defFileType?: DefFileType;

	constructor(app: App, file: TFile) {
		this.app = app;
		this.file = file;
	}

	// Optional argument used when file cache may not be updated
	// and we know the new contents of the file
	async parseFile(fileContent?: string): Promise<Definition[]> {
		this.defFileType = await this.getDefFileType();

		switch (this.defFileType) {
			case DefFileType.Consolidated:
				const defParser = new ConsolidatedDefParser(
					this.app,
					this.file,
				);
				return defParser.parseFile(fileContent);
			case DefFileType.Atomic:
				const atomicParser = new AtomicDefParser(this.app, this.file);
				return atomicParser.parseFile(fileContent);
		}
	}

	private async getDefFileType(): Promise<DefFileType> {
		let fileCache: CachedMetadata | null = null;
		// fileCache may return nil at on Obsidian startup. Obsidian likely needs some time to warm up the cache
		const retry = useRetry();
		await retry.exec(() => {
			fileCache = this.app.metadataCache.getFileCache(this.file);
			if (!fileCache) {
				retry.setShouldRetry();
			}
		});
		// @ts-ignore: fileCache should be set in the closure above
		const fmFileType = fileCache?.frontmatter?.[DEF_TYPE_FM];
		if (
			fmFileType &&
			(fmFileType === DefFileType.Consolidated ||
				fmFileType === DefFileType.Atomic)
		) {
			return fmFileType;
		}

		// Fallback to configured default
		const parserSettings = getSettings().defFileParseConfig;

		if (parserSettings.defaultFileType) {
			return parserSettings.defaultFileType;
		}
		return DefFileType.Consolidated;
	}
}


================================================
FILE: src/core/file-type.ts
================================================
export enum DefFileType {
	Consolidated = "consolidated",
	Atomic = "atomic",
}


================================================
FILE: src/core/fm-builder.ts
================================================
export class FrontmatterBuilder {
	fm: Map<string, string>;

	constructor() {
		this.fm = new Map<string, string>();
	}

	add(k: string, v: string) {
		this.fm.set(k, v);
	}

	finish(): string {
		let fm = "---\n";
		this.fm.forEach((v, k) => {
			fm += `${k}: ${v}\n`;
		});
		fm += "---\n";
		return fm;
	}
}


================================================
FILE: src/core/model.ts
================================================
import { TFile } from "obsidian";
import { DefFileType } from "./file-type";

export interface Definition {
	key: string;
	word: string;
	aliases: string[];
	definition: string;
	file: TFile;
	linkText: string;
	fileType: DefFileType;
	position?: FilePosition;
}

// Both to and from inclusive
export interface FilePosition {
	from: number;
	to: number;
}


================================================
FILE: src/editor/add-modal.ts
================================================
import {
	App,
	DropdownComponent,
	Modal,
	Notice,
	Setting,
	TFile,
} from "obsidian";
import { getDefFileManager, DEF_CTX_FM_KEY } from "src/core/def-file-manager";
import { DefFileUpdater } from "src/core/def-file-updater";
import { DefFileType } from "src/core/file-type";

export class AddDefinitionModal {
	app: App;
	activeFile: TFile | null;
	modal: Modal;
	aliases: string;
	definition: string;
	submitting: boolean;

	fileTypePicker: DropdownComponent;
	defFilePickerSetting: Setting;
	defFilePicker: DropdownComponent;

	atomicFolderPickerSetting: Setting;
	atomicFolderPicker: DropdownComponent;

	constructor(app: App) {
		this.app = app;
		this.modal = new Modal(app);
	}

	async open(text?: string) {
		// initialize the view when the modal is opened to ensure it's up to date
		this.activeFile = this.app.workspace.getActiveFile();

		this.submitting = false;

		// create modal content
		this.modal.setTitle("Add Definition");
		this.modal.contentEl.createDiv({
			cls: "edit-modal-section-header",
			text: "Word/Phrase",
		});
		const phraseText = this.modal.contentEl.createEl("textarea", {
			cls: "edit-modal-aliases",
			attr: {
				placeholder: "Word/phrase to be defined",
			},
			text: text ?? "",
		});
		this.modal.contentEl.createDiv({
			cls: "edit-modal-section-header",
			text: "Aliases",
		});
		const aliasText = this.modal.contentEl.createEl("textarea", {
			cls: "edit-modal-aliases",
			attr: {
				placeholder: "Add comma-separated aliases here",
			},
		});
		this.modal.contentEl.createDiv({
			cls: "edit-modal-section-header",
			text: "Definition",
		});
		const defText = this.modal.contentEl.createEl("textarea", {
			cls: "edit-modal-textarea",
			attr: {
				placeholder: "Add definition here",
			},
		});

		// create definition file picker
		const defManager = getDefFileManager();

		// Get the most updated def files and folders on modal open
		let [defFolders, defFiles] = defManager.getDefFilesAndFolders();

		if (defFolders.length === 0) {
			await this.app.vault.createFolder(defManager.getGlobalDefFolder());
			await this.app.vault.create(
				`${defManager.getGlobalDefFolder()}/definitions.md`,
				"",
			);
			const f = defManager.getDefFilesAndFolders();
			defFolders = f[0];
			defFiles = f[1];
		}

		// get the currently opened file's first folder and first file, if they exist
		let default_def_file = "";
		let default_def_folder = "";
		let paths: Array<string> = [];

		// if the currently open file has at least one definition context, use it's
		// first context as the initial value
		if (this.activeFile) {
			const metadataCache = this.app.metadataCache.getFileCache(
				this.activeFile,
			);
			paths = metadataCache?.frontmatter?.[DEF_CTX_FM_KEY];
			if (paths) {
				// get the first folder in the path (if it exists) - use regexp to remove the trailing
				// `/` that might be present
				default_def_folder =
					paths.find(
						(path: string) =>
							this.app.vault.getFolderByPath(
								path.replace(/\/+$/, ""),
							) != null,
					) || "";
				if (default_def_folder) {
					default_def_folder = default_def_folder.replace(/\/+$/, "");
				}

				// get the first file in the path (if it exists)
				default_def_file =
					paths.find(
						(path: string) =>
							this.app.vault.getFileByPath(path) != null,
					) || "";
			}
		}

		this.defFilePickerSetting = new Setting(this.modal.contentEl)
			.setName("Definition file")
			.addDropdown((component) => {
				defFiles.forEach((file) => {
					component.addOption(file.path, file.path);
				});

				// use the first definition file from this file's metadata, or default to
				// the first consolidated def file in the list if it exists
				if (default_def_file) {
					component.setValue(default_def_file);
				} else if (defFiles.length > 0) {
					component.setValue(defFiles[0].path);
				}

				this.defFilePicker = component;
			});

		this.atomicFolderPickerSetting = new Setting(this.modal.contentEl)
			.setName("Add file to folder")
			.addDropdown((component) => {
				defFolders.forEach((folder) => {
					component.addOption(folder.path, folder.path + "/");
				});

				// use the first definition folder from this file's metadata, or default to
				// the first folder in the list if it exists
				if (default_def_folder) {
					component.setValue(default_def_folder);
				} else if (defFolders.length > 0) {
					component.setValue(defFolders[0].path);
				}

				this.atomicFolderPicker = component;
			});

		new Setting(this.modal.contentEl)
			.setName("Definition file type")
			.addDropdown((component) => {
				const handleDefFileTypeChange = (val: string) => {
					if (val === DefFileType.Consolidated) {
						this.atomicFolderPickerSetting.settingEl.hide();
						this.defFilePickerSetting.settingEl.show();
					} else if (val === DefFileType.Atomic) {
						this.defFilePickerSetting.settingEl.hide();
						this.atomicFolderPickerSetting.settingEl.show();
					}
				};

				component.addOption(DefFileType.Consolidated, "Consolidated");
				component.addOption(DefFileType.Atomic, "Atomic");

				// use the default definition type as a fallback
				component.setValue(
					window.NoteDefinition.settings.defFileParseConfig
						.defaultFileType,
				);

				component.onChange(handleDefFileTypeChange);
				handleDefFileTypeChange(component.getValue());
				this.fileTypePicker = component;
			});

		const button = this.modal.contentEl.createEl("button", {
			text: "Save",
			cls: "edit-modal-save-button",
		});

		button.addEventListener("click", () => {
			this.try_submit(phraseText, defText, aliasText);
		});

		// set up key event listeners for closing and submitting the modal
		this.modal.scope.register(["Mod"], "Enter", () => {
			this.try_submit(phraseText, defText, aliasText);
		});

		this.modal.open();
	}

	// Checks if the requirements for a definition (name, description, file) have been met,
	// showing an error notification if they haven't. Creates the definition and closes the modal
	// if there aren't any issues.
	try_submit(
		phraseText: HTMLTextAreaElement,
		defText: HTMLTextAreaElement,
		aliasText: HTMLTextAreaElement,
	) {
		// we're already submitting the definition
		if (this.submitting) {
			return;
		}

		// invalid definition paramters (missing name or description)
		if (!phraseText.value || !defText.value) {
			new Notice("Please fill in a definition value");
			return;
		}

		const fileType = this.fileTypePicker.getValue();
		let selectedPath = "";
		let definitionFile;

		if (fileType === DefFileType.Consolidated) {
			selectedPath = this.defFilePicker.getValue();
			if (!selectedPath) {
				new Notice(
					"Please choose a definition file. If you do not have any definition files, please create one.",
				);
				return;
			}
			const defFileManager = getDefFileManager();
			definitionFile = defFileManager.globalDefFiles.get(selectedPath);
		} else if (fileType === DefFileType.Atomic) {
			selectedPath = this.atomicFolderPicker.getValue();
			if (!selectedPath) {
				new Notice("Please choose a folder for the atomic definition.");
				return;
			}
			definitionFile = undefined;
		} else {
			new Notice("Invalid file type selected.");
			return;
		}

		const updated = new DefFileUpdater(this.app);
		updated.addDefinition(
			{
				fileType: fileType as DefFileType,
				key: phraseText.value.toLowerCase(),
				word: phraseText.value,
				aliases: aliasText.value
					? aliasText.value.split(",").map((alias) => alias.trim())
					: [],
				definition: defText.value,
				file: definitionFile,
			},
			selectedPath,
		);
		this.modal.close();
	}
}


================================================
FILE: src/editor/common.ts
================================================
import { Platform } from "obsidian";
import { getSettings, PopoverEventSettings } from "src/settings";

const triggerFunc =
	"event.stopPropagation();activeWindow.NoteDefinition.triggerDefPreview(this);";

export const DEF_DECORATION_CLS = "def-decoration";

// For normal decoration of definitions
export function getDecorationAttrs(phrase: string): { [key: string]: string } {
	const attributes: { [key: string]: string } = {
		def: phrase,
	};
	const settings = getSettings();
	if (Platform.isMobile) {
		attributes.onclick = triggerFunc;
		return attributes;
	}
	if (settings.popoverEvent === PopoverEventSettings.Click) {
		attributes.onclick = triggerFunc;
	} else {
		attributes.onmouseenter = triggerFunc;
	}
	if (!settings.enableSpellcheck) {
		attributes.spellcheck = "false";
	}
	return attributes;
}


================================================
FILE: src/editor/decoration.ts
================================================
import { RangeSetBuilder } from "@codemirror/state";
import {
	Decoration,
	DecorationSet,
	EditorView,
	PluginSpec,
	PluginValue,
	ViewPlugin,
	ViewUpdate,
} from "@codemirror/view";
import { logDebug } from "src/util/log";
import { DEF_DECORATION_CLS, getDecorationAttrs } from "./common";
import { LineScanner } from "./definition-search";
import { PTreeNode } from "./prefix-tree";

// Information of phrase that can be used to add decorations within the editor
interface PhraseInfo {
	from: number;
	to: number;
	phrase: string;
}

let markedPhrases: PhraseInfo[] = [];

export function getMarkedPhrases(): PhraseInfo[] {
	return markedPhrases;
}

// View plugin to mark definitions
export class DefinitionMarker implements PluginValue {
	decorations: DecorationSet;
	editorView: EditorView;

	constructor(view: EditorView) {
		this.editorView = view;
		this.decorations = this.buildDecorations(view);
	}

	update(update: ViewUpdate) {
		if (
			update.docChanged ||
			update.viewportChanged ||
			update.focusChanged
		) {
			const start = performance.now();
			this.decorations = this.buildDecorations(update.view);
			const end = performance.now();
			logDebug(`Marked definitions in ${end - start}ms`);
			return;
		}
	}

	public forceUpdate() {
		const start = performance.now();
		this.decorations = this.buildDecorations(this.editorView);
		const end = performance.now();
		logDebug(`Marked definitions in ${end - start}ms`);
		return;
	}

	destroy() {}

	buildDecorations(view: EditorView): DecorationSet {
		const builder = new RangeSetBuilder<Decoration>();
		const phraseInfos: PhraseInfo[] = [];

		for (let { from, to } of view.visibleRanges) {
			const text = view.state.sliceDoc(from, to);
			phraseInfos.push(...scanText(text, from));
		}

		phraseInfos.forEach((wordPos) => {
			const attributes = getDecorationAttrs(wordPos.phrase);
			builder.add(
				wordPos.from,
				wordPos.to,
				Decoration.mark({
					class: DEF_DECORATION_CLS,
					attributes: attributes,
				}),
			);
		});

		markedPhrases = phraseInfos;
		return builder.finish();
	}
}

// Scan text and return phrases and their positions that require decoration
export function scanText(
	text: string,
	offset: number,
	pTree?: PTreeNode,
): PhraseInfo[] {
	let phraseInfos: PhraseInfo[] = [];
	const lines = text.split(/\r?\n/);
	let internalOffset = offset;
	const lineScanner = new LineScanner(pTree);

	lines.forEach((line) => {
		phraseInfos.push(...lineScanner.scanLine(line, internalOffset));
		// Additional 1 char for \n char
		internalOffset += line.length + 1;
	});

	// Decorations need to be sorted by 'from' ascending, then 'to' descending
	// This allows us to prefer longer words over shorter ones
	phraseInfos.sort((a, b) => b.to - a.to);
	phraseInfos.sort((a, b) => a.from - b.from);
	return removeSubsetsAndIntersects(phraseInfos);
}

function removeSubsetsAndIntersects(phraseInfos: PhraseInfo[]): PhraseInfo[] {
	let cursor = 0;
	return phraseInfos.filter((phraseInfo) => {
		if (phraseInfo.from >= cursor) {
			cursor = phraseInfo.to;
			return true;
		}
		return false;
	});
}

const pluginSpec: PluginSpec<DefinitionMarker> = {
	decorations: (value: DefinitionMarker) => value.decorations,
};

export const definitionMarker = ViewPlugin.fromClass(
	DefinitionMarker,
	pluginSpec,
);


================================================
FILE: src/editor/def-file-registration.ts
================================================
import { App, TFile } from "obsidian";
import { getDefFileManager } from "src/core/def-file-manager";
import { DEF_TYPE_FM } from "src/core/file-parser";
import { DefFileType } from "src/core/file-type";
import { logError } from "src/util/log";

export function registerDefFile(app: App, file: TFile, fileType: DefFileType) {
	app.fileManager
		.processFrontMatter(file, (fm) => {
			fm[DEF_TYPE_FM] = fileType;
			getDefFileManager().loadDefinitions();
		})
		.catch((e) => {
			logError(`Err writing to frontmatter of file: ${e}`);
		});
}


================================================
FILE: src/editor/definition-popover.ts
================================================
import {
	App,
	ButtonComponent,
	Component,
	MarkdownRenderer,
	MarkdownView,
	normalizePath,
	Plugin,
} from "obsidian";
import { Definition } from "src/core/model";
import { getSettings, PopoverDismissType } from "src/settings";
import { logDebug, logError } from "src/util/log";

const DEF_POPOVER_ID = "definition-popover";

let definitionPopover: DefinitionPopover;

interface Coordinates {
	left: number;
	right: number;
	top: number;
	bottom: number;
}

export class DefinitionPopover extends Component {
	app: App;
	plugin: Plugin;
	// Code mirror editor object for capturing vim events
	cmEditor: any;
	// Ref to the currently mounted popover
	// There should only be one mounted popover at all times
	mountedPopover: HTMLElement | undefined;

	constructor(plugin: Plugin) {
		super();
		this.app = plugin.app;
		this.plugin = plugin;
		this.cmEditor = this.getCmEditor(this.app);
	}

	// Open at editor cursor's position
	openAtCursor(def: Definition) {
		this.unmount();
		this.mountAtCursor(def);

		if (!this.mountedPopover) {
			logError("Mounting definition popover failed");
			return;
		}

		this.registerClosePopoverListeners();
	}

	// Open at coordinates (can use for opening at mouse position)
	openAtCoords(def: Definition, coords: Coordinates) {
		this.unmount();
		this.mountAtCoordinates(def, coords);

		if (!this.mountedPopover) {
			logError("mounting definition popover failed");
			return;
		}
		this.registerClosePopoverListeners();
	}

	cleanUp() {
		logDebug("Cleaning popover elements");
		const popoverEls = document.getElementsByClassName(DEF_POPOVER_ID);
		for (let i = 0; i < popoverEls.length; i++) {
			popoverEls[i].remove();
		}
	}

	close = () => {
		this.unmount();
	};

	clickClose = () => {
		if (this.mountedPopover?.matches(":hover")) {
			return;
		}
		this.close();
	};

	private getCmEditor(app: App) {
		const activeView = app.workspace.getActiveViewOfType(MarkdownView);
		const cmEditor = (activeView as any)?.editMode?.editor?.cm?.cm;
		if (!cmEditor) {
			logDebug(
				"cmEditor object not found, will not handle vim events for definition popover",
			);
		}
		return cmEditor;
	}

	private shouldOpenToLeft(
		horizontalOffset: number,
		containerStyle: CSSStyleDeclaration,
	): boolean {
		return horizontalOffset > parseInt(containerStyle.width) / 2;
	}

	private shouldOpenUpwards(
		verticalOffset: number,
		containerStyle: CSSStyleDeclaration,
	): boolean {
		return verticalOffset > parseInt(containerStyle.height) / 2;
	}

	// Creates popover element and its children, without displaying it
	private createElement(
		def: Definition,
		parent: HTMLElement,
	): HTMLDivElement {
		const popoverSettings = getSettings().defPopoverConfig;
		const el = parent.createEl("div", {
			cls: "definition-popover",
			attr: {
				id: DEF_POPOVER_ID,
				style: `visibility:hidden;${
					popoverSettings.backgroundColour
						? `background-color: ${popoverSettings.backgroundColour};`
						: ""
				}`,
			},
		});

		// create a button linking to the definition
		new ButtonComponent(el)
			.setIcon("arrow-left-from-line")
			.setTooltip("Go to definition")
			.setClass("popover-go-to-def-button")
			.onClick(() => {
				this.app.workspace.openLinkText(def.linkText, "");
			});

		el.createEl("h2", { text: def.word });
		if (def.aliases.length > 0 && popoverSettings.displayAliases) {
			el.createEl("i", { text: def.aliases.join(", ") });
		}
		const contentEl = el.createEl("div");
		contentEl.setAttr("ctx", "def-popup");

		const currComponent = this;
		MarkdownRenderer.render(
			this.app,
			def.definition,
			contentEl,
			normalizePath(def.file.path),
			currComponent,
		);
		this.postprocessMarkdown(contentEl, def);

		if (popoverSettings.displayDefFileName) {
			el.createEl("div", {
				text: def.file.basename,
				cls: "definition-popover-filename",
			});
		}
		return el;
	}

	// Internal links do not work properly in the popover
	// This is to manually open internal links
	private postprocessMarkdown(el: HTMLDivElement, def: Definition) {
		const internalLinks = el.getElementsByClassName("internal-link");
		for (let i = 0; i < internalLinks.length; i++) {
			const linkEl = internalLinks.item(i);
			if (linkEl) {
				linkEl.addEventListener("click", (e) => {
					e.preventDefault();
					const file = this.app.metadataCache.getFirstLinkpathDest(
						linkEl.getAttr("href") ?? "",
						normalizePath(def.file.path),
					);
					this.unmount();
					if (!file) {
						return;
					}
					this.app.workspace.getLeaf().openFile(file);
				});
			}
		}
	}

	private mountAtCursor(def: Definition) {
		let cursorCoords;
		try {
			cursorCoords = this.getCursorCoords();
		} catch (e) {
			logError(
				"Could not open definition popover - could not get cursor coordinates",
			);
			return;
		}

		this.mountAtCoordinates(def, cursorCoords);
	}

	// Offset coordinates from viewport coordinates to coordinates relative to the parent container element
	private offsetCoordsToContainer(
		coords: Coordinates,
		container: HTMLElement,
	): Coordinates {
		const containerRect = container.getBoundingClientRect();
		return {
			left: coords.left - containerRect.left,
			right: coords.right - containerRect.left,
			top: coords.top - containerRect.top,
			bottom: coords.bottom - containerRect.top,
		};
	}

	private mountAtCoordinates(def: Definition, coords: Coordinates) {
		const mdView = this.app.workspace.getActiveViewOfType(MarkdownView);
		if (!mdView) {
			logError("Could not mount popover: No active markdown view found");
			return;
		}

		this.mountedPopover = this.createElement(def, mdView.containerEl);
		this.positionAndSizePopover(mdView, coords);
	}

	// Position and display popover
	private positionAndSizePopover(mdView: MarkdownView, coords: Coordinates) {
		if (!this.mountedPopover) {
			return;
		}
		const popoverSettings = getSettings().defPopoverConfig;
		const containerStyle = getComputedStyle(mdView.containerEl);
		const matchedClasses =
			mdView.containerEl.getElementsByClassName("view-header");
		// The container div has a header element that needs to be accounted for
		let offsetHeaderHeight = 0;
		if (matchedClasses.length > 0) {
			offsetHeaderHeight = parseInt(
				getComputedStyle(matchedClasses[0]).height,
			);
		}

		// Offset coordinates to be relative to container
		coords = this.offsetCoordsToContainer(coords, mdView.containerEl);

		const positionStyle: Partial<CSSStyleDeclaration> = {
			visibility: "visible",
		};

		positionStyle.maxWidth =
			popoverSettings.enableCustomSize && popoverSettings.maxWidth
				? `${popoverSettings.maxWidth}px`
				: `${parseInt(containerStyle.width) / 2}px`;
		if (this.shouldOpenToLeft(coords.left, containerStyle)) {
			positionStyle.right = `${parseInt(containerStyle.width) - coords.right}px`;
		} else {
			positionStyle.left = `${coords.left}px`;
		}

		if (this.shouldOpenUpwards(coords.top, containerStyle)) {
			positionStyle.bottom = `${parseInt(containerStyle.height) - coords.top}px`;
			positionStyle.maxHeight =
				popoverSettings.enableCustomSize && popoverSettings.maxHeight
					? `${popoverSettings.maxHeight}px`
					: `${coords.top - offsetHeaderHeight}px`;
		} else {
			positionStyle.top = `${coords.bottom}px`;
			positionStyle.maxHeight =
				popoverSettings.enableCustomSize && popoverSettings.maxHeight
					? `${popoverSettings.maxHeight}px`
					: `${parseInt(containerStyle.height) - coords.bottom}px`;
		}

		this.mountedPopover.setCssStyles(positionStyle);
	}

	private unmount() {
		if (!this.mountedPopover) {
			logDebug("Nothing to unmount, could not find popover element");
			return;
		}
		this.mountedPopover.remove();
		this.mountedPopover = undefined;

		this.unregisterClosePopoverListeners();
	}

	// This uses internal non-exposed codemirror API to get cursor coordinates
	// Cursor coordinates seem to be relative to viewport
	private getCursorCoords(): Coordinates {
		const editor = this.app.workspace.activeEditor?.editor;
		// @ts-ignore
		return editor?.cm?.coordsAtPos(
			editor?.posToOffset(editor?.getCursor()),
			-1,
		);
	}

	private registerClosePopoverListeners() {
		this.getActiveView()?.containerEl.addEventListener(
			"keydown",
			this.close,
		);
		this.getActiveView()?.containerEl.addEventListener(
			"click",
			this.clickClose,
		);

		if (this.mountedPopover) {
			this.mountedPopover.addEventListener("mouseleave", () => {
				const popoverSettings = getSettings().defPopoverConfig;
				if (
					popoverSettings.popoverDismissEvent ===
					PopoverDismissType.MouseExit
				) {
					this.clickClose();
				}
			});
		}
		if (this.cmEditor) {
			this.cmEditor.on("vim-keypress", this.close);
		}
		const scroller = this.getCmScroller();
		if (scroller) {
			scroller.addEventListener("scroll", this.close);
		}
	}

	private unregisterClosePopoverListeners() {
		this.getActiveView()?.containerEl.removeEventListener(
			"keypress",
			this.close,
		);
		this.getActiveView()?.containerEl.removeEventListener(
			"click",
			this.clickClose,
		);

		if (this.cmEditor) {
			this.cmEditor.off("vim-keypress", this.close);
		}
		const scroller = this.getCmScroller();
		if (scroller) {
			scroller.removeEventListener("scroll", this.close);
		}
	}

	private getCmScroller() {
		const scroller = document.getElementsByClassName("cm-scroller");
		if (scroller.length > 0) {
			return scroller[0];
		}
	}

	getPopoverElement() {
		return document.getElementById("definition-popover");
	}

	private getActiveView() {
		return this.app.workspace.getActiveViewOfType(MarkdownView);
	}
}

// Mount definition popover
export function initDefinitionPopover(plugin: Plugin) {
	if (definitionPopover) {
		definitionPopover.cleanUp();
	}
	definitionPopover = new DefinitionPopover(plugin);
}

export function getDefinitionPopover() {
	return definitionPopover;
}


================================================
FILE: src/editor/definition-search.ts
================================================
import { getDefFileManager } from "src/core/def-file-manager";
import { PTreeNode, PTreeTraverser } from "./prefix-tree";
import { getSettings } from "src/settings";

// Information of phrase that can be used to add decorations within the editor
export interface PhraseInfo {
	from: number;
	to: number;
	phrase: string;
}

export class LineScanner {
	prefixTree: PTreeNode;

	private cnLangRegex = /\p{Script=Han}/u;
	private terminatingCharRegex =
		/[!@#$%^&*()\+={}[\]:;"'<>,.?\/|\\\r\n ()*+,-/:;<=>@[\]^_`{|}~⦅⦆「」、 、〃〈〉《》「」『』【】〔〕〖〗〘〙〚〛〜〝〞〟—‘’‛“”„‟…‧﹏﹑﹔·。]/;

	constructor(pTree?: PTreeNode) {
		this.prefixTree = pTree ? pTree : getDefFileManager().getPrefixTree();
	}

	scanLine(line: string, offset?: number): PhraseInfo[] {
		let traversers: PTreeTraverser[] = [];
		const phraseInfos: PhraseInfo[] = [];

		for (let i = 0; i < line.length; i++) {
			let c="";
			if (getSettings().defFileParseConfig.enableCaseSensitive) {
				c = line.charAt(i);
			}
			else {
				c = line.charAt(i).toLowerCase();
			}
			if (this.isValidStart(line, i)) {
				traversers.push(new PTreeTraverser(this.prefixTree));
			}

			traversers.forEach((traverser) => {
				traverser.gotoNext(c);
				if (traverser.isWordEnd() && this.isValidEnd(line, i)) {
					const phrase = traverser.getWord();
					phraseInfos.push({
						phrase: phrase,
						from: (offset ?? 0) + i - phrase.length + 1,
						to: (offset ?? 0) + i + 1,
					});
				}
			});

			// Collect garbage traversers that hit a dead-end
			traversers = traversers.filter((traverser) => {
				return !!traverser.currPtr;
			});
		}
		return phraseInfos;
	}

	private isValidEnd(line: string, ptr: number): boolean {
		let c="";
		if (getSettings().defFileParseConfig.enableCaseSensitive) {
			c = line.charAt(ptr);
		}
		else {
			c = line.charAt(ptr).toLowerCase();
		}
		if (this.isNonSpacedLanguage(c)) {
			return true;
		}
		// If EOL, then it is a valid end
		if (ptr === line.length - 1) {
			return true;
		}
		// Check if next character is a terminating character
		return this.terminatingCharRegex.test(line.charAt(ptr + 1));
	}

	// Check if this character is a valid start of a word depending on the context
	private isValidStart(line: string, ptr: number): boolean {
		let c="";
		if (getSettings().defFileParseConfig.enableCaseSensitive) {
			c = line.charAt(ptr);
		}
		else {
			c = line.charAt(ptr).toLowerCase();
		}
		if (c == " ") {
			return false;
		}
		if (ptr === 0 || this.isNonSpacedLanguage(c)) {
			return true;
		}
		// Check if previous character is a terminating character
		return this.terminatingCharRegex.test(line.charAt(ptr - 1));
	}

	private isNonSpacedLanguage(c: string): boolean {
		return this.cnLangRegex.test(c);
	}
}


================================================
FILE: src/editor/edit-modal.ts
================================================
import { App, Modal } from "obsidian";
import { DefFileUpdater } from "src/core/def-file-updater";
import { Definition } from "src/core/model";

export class EditDefinitionModal {
	app: App;
	modal: Modal;
	aliases: string;
	definition: string;
	submitting: boolean;

	constructor(app: App) {
		this.app = app;
		this.modal = new Modal(app);
	}

	open(def: Definition) {
		this.submitting = false;
		this.modal.setTitle(`Edit definition for '${def.word}'`);
		this.modal.contentEl.createDiv({
			cls: "edit-modal-section-header",
			text: "Aliases",
		});
		const aliasText = this.modal.contentEl.createEl("textarea", {
			cls: "edit-modal-aliases",
			attr: {
				placeholder: "Add comma-separated aliases here",
			},
			text: def.aliases.join(", "),
		});
		this.modal.contentEl.createDiv({
			cls: "edit-modal-section-header",
			text: "Definition",
		});
		const defText = this.modal.contentEl.createEl("textarea", {
			cls: "edit-modal-textarea",
			attr: {
				placeholder: "Add definition here",
			},
			text: def.definition,
		});
		const button = this.modal.contentEl.createEl("button", {
			text: "Save",
			cls: "edit-modal-save-button",
		});
		button.addEventListener("click", () => {
			this.submit(def, aliasText, defText);
		});

		// set up key event listeners for closing and submitting the modal
		this.modal.scope.register(["Mod"], "Enter", () => {
			this.submit(def, defText, aliasText);
		});

		this.modal.open();
	}

	submit(
		def: Definition,
		aliasText: HTMLTextAreaElement,
		defText: HTMLTextAreaElement,
	) {
		if (this.submitting) {
			return;
		}
		const updater = new DefFileUpdater(this.app);
		updater.updateDefinition({
			...def,
			aliases: aliasText.value
				? aliasText.value.split(",").map((alias) => alias.trim())
				: [],
			definition: defText.value,
		});
		this.modal.close();
	}
}


================================================
FILE: src/editor/frontmatter-suggest-modal.ts
================================================
import {
	App,
	FuzzySuggestModal,
	Notice,
	TAbstractFile,
	TFile,
	TFolder,
} from "obsidian";
import { DEF_CTX_FM_KEY, getDefFileManager } from "src/core/def-file-manager";
import { logError } from "src/util/log";

export class FMSuggestModal extends FuzzySuggestModal<TAbstractFile> {
	file: TFile;

	constructor(app: App, currFile: TFile) {
		super(app);
		this.file = currFile;
	}

	getItems(): TAbstractFile[] {
		const defManager = getDefFileManager();
		return [...defManager.getDefFiles(), ...defManager.getDefFolders()];
	}

	getItemText(item: TAbstractFile): string {
		return this.getPath(item);
	}

	onChooseItem(item: TAbstractFile, evt: MouseEvent | KeyboardEvent) {
		const path = this.getPath(item);
		this.app.fileManager
			.processFrontMatter(this.file, (fm) => {
				let currDefSource = fm[DEF_CTX_FM_KEY];

				if (!currDefSource || !Array.isArray(currDefSource)) {
					currDefSource = [];
				} else if (currDefSource.includes(path)) {
					new Notice(
						"Definition file source is already included for this file",
					);
					return;
				}

				fm[DEF_CTX_FM_KEY] = [...currDefSource, path];
			})
			.catch((e) => {
				logError(`Error writing to frontmatter of file: ${e}`);
			});
	}

	private getPath(file: TAbstractFile): string {
		if (file instanceof TFolder) {
			return file.path + "/";
		}
		return file.path;
	}
}


================================================
FILE: src/editor/md-postprocessor.ts
================================================
import { MarkdownPostProcessor } from "obsidian";
import { getDefFileManager } from "src/core/def-file-manager";
import { getSettings } from "src/settings";
import { DEF_DECORATION_CLS, getDecorationAttrs } from "./common";
import { getDefinitionPopover } from "./definition-popover";
import { LineScanner, PhraseInfo } from "./definition-search";

const DEF_LINK_DECOR_CLS = "def-link-decoration";

interface Marks {
	el: HTMLElement;
	phraseInfo: PhraseInfo;
}

export const postProcessor: MarkdownPostProcessor = (element, context) => {
	const shouldRunPostProcessor =
		window.NoteDefinition.settings.enableInReadingView;
	if (!shouldRunPostProcessor) {
		return;
	}

	const popoverSettings = getSettings().defPopoverConfig;

	// Prevent post-processing for definition popover
	const isPopupCtx = element.getAttr("ctx") === "def-popup";
	if (isPopupCtx && !popoverSettings.enableDefinitionLink) {
		return;
	}

	rebuildHTML(element, isPopupCtx);
};

const rebuildHTML = (parent: Node, isPopupCtx: boolean) => {
	// Skip the function entirely (including recursion into node's children) to disable formatting on links
	if (!getSettings().enableOnLinks && parent.nodeName === "A") {
		return;
	}

	for (let i = 0; i < parent.childNodes.length; i++) {
		const childNode = parent.childNodes[i];
		// Replace only if TEXT_NODE
		if (childNode.nodeType === Node.TEXT_NODE && childNode.textContent) {
			if (childNode.textContent === "\n") {
				// Ignore nodes with just a newline char
				continue;
			}

			const lineScanner = new LineScanner();
			const currText = childNode.textContent;
			const phraseInfos = lineScanner.scanLine(currText);
			if (phraseInfos.length === 0) {
				continue;
			}

			// Decorations need to be sorted by 'from' ascending, then 'to' descending
			// This allows us to prefer longer words over shorter ones
			phraseInfos.sort((a, b) => b.to - a.to);
			phraseInfos.sort((a, b) => a.from - b.from);

			let currCursor = 0;
			const newContainer = parent.createSpan();
			const addedMarks: Marks[] = [];

			const popoverSettings = getSettings().defPopoverConfig;

			phraseInfos.forEach((phraseInfo) => {
				if (phraseInfo.from < currCursor) {
					// Subset or intersect phrases are ignored
					return;
				}

				newContainer.appendText(
					currText.slice(currCursor, phraseInfo.from),
				);

				let span: HTMLSpanElement;
				if (isPopupCtx && popoverSettings.enableDefinitionLink) {
					span = getLinkDecorationSpan(
						newContainer,
						phraseInfo,
						currText,
					);
				} else {
					span = getNormalDecorationSpan(
						newContainer,
						phraseInfo,
						currText,
					);
				}

				newContainer.appendChild(span);
				addedMarks.push({
					el: span,
					phraseInfo: phraseInfo,
				});
				currCursor = phraseInfo.to;
			});

			newContainer.appendText(currText.slice(currCursor));
			childNode.replaceWith(newContainer);
		}

		rebuildHTML(childNode, isPopupCtx);
	}
};

function getNormalDecorationSpan(
	container: HTMLElement,
	phraseInfo: PhraseInfo,
	currText: string,
): HTMLSpanElement {
	const attributes = getDecorationAttrs(phraseInfo.phrase);
	const span = container.createSpan({
		cls: DEF_DECORATION_CLS,
		attr: attributes,
		text: currText.slice(phraseInfo.from, phraseInfo.to),
	});
	return span;
}

function getLinkDecorationSpan(
	container: HTMLElement,
	phraseInfo: PhraseInfo,
	currText: string,
): HTMLSpanElement {
	const span = container.createSpan({
		cls: DEF_LINK_DECOR_CLS,
		text: currText.slice(phraseInfo.from, phraseInfo.to),
	});
	span.addEventListener("click", (e) => {
		const app = window.NoteDefinition.app;
		const def = getDefFileManager().get(phraseInfo.phrase);
		if (!def) {
			return;
		}
		app.workspace.openLinkText(def.linkText, "");
		// Close definition popover
		const popover = getDefinitionPopover();
		if (popover) {
			popover.close();
		}
	});
	return span;
}


================================================
FILE: src/editor/mobile/definition-modal.ts
================================================
import {
	App,
	Component,
	MarkdownRenderer,
	normalizePath,
	Modal,
} from "obsidian";
import { Definition } from "src/core/model";

let defModal: DefinitionModal;

export class DefinitionModal extends Component {
	app: App;
	modal: Modal;

	constructor(app: App) {
		super();
		this.app = app;
		this.modal = new Modal(app);
	}

	open(definition: Definition) {
		this.modal.contentEl.empty();
		this.modal.contentEl.createEl("h1", {
			text: definition.word,
		});
		this.modal.contentEl.createEl("i", {
			text: definition.aliases.join(", "),
		});
		const defContent = this.modal.contentEl.createEl("div", {
			attr: {
				ctx: "def-popup",
			},
		});
		MarkdownRenderer.render(
			this.app,
			definition.definition,
			defContent,
			normalizePath(definition.file.path) ?? "",
			this,
		);
		this.modal.open();
	}
}

export function initDefinitionModal(app: App) {
	defModal = new DefinitionModal(app);
	return defModal;
}

export function getDefinitionModal() {
	return defModal;
}


================================================
FILE: src/editor/prefix-tree.ts
================================================
// Prefix tree node
export class PTreeNode {
	children: Map<string, PTreeNode>;
	wordEnd: boolean;

	constructor() {
		this.children = new Map<string, PTreeNode>();
		this.wordEnd = false;
	}

	add(word: string, ptr?: number) {
		if (ptr === undefined) {
			ptr = 0;
		}
		if (ptr === word.length) {
			this.wordEnd = true;
			return;
		}
		const currChar = word.charAt(ptr);
		let nextNode;
		nextNode = this.children.get(currChar);
		if (!nextNode) {
			nextNode = new PTreeNode();
			this.children.set(currChar, nextNode);
		}
		nextNode.add(word, ++ptr);
	}
}

// A traverser implementation to traverse the prefix tree and keep track of states
export class PTreeTraverser {
	currPtr?: PTreeNode;
	wordBuf: Array<string>;

	constructor(root: PTreeNode) {
		this.currPtr = root;
		this.wordBuf = [];
	}

	gotoNext(c: string) {
		if (!this.currPtr) {
			return;
		}
		const nextNode = this.currPtr.children.get(c);
		// This will set currPtr to undefined if there is no next node
		// This marks the traverser as garbage to be collected
		this.currPtr = nextNode;
		this.wordBuf.push(c);
	}

	isWordEnd() {
		if (!this.currPtr) {
			return false;
		}
		return this.currPtr.wordEnd;
	}

	getWord() {
		return this.wordBuf.join("");
	}
}


================================================
FILE: src/globals.ts
================================================
import { App, Platform } from "obsidian";
import { DefinitionRepo, getDefFileManager } from "./core/def-file-manager";
import { getDefinitionPopover } from "./editor/definition-popover";
import { getDefinitionModal } from "./editor/mobile/definition-modal";
import { getSettings, PopoverDismissType, Settings } from "./settings";
import { LogLevel } from "./util/log";

export {};

declare global {
	interface Window {
		NoteDefinition: GlobalVars;
	}
}

export interface GlobalVars {
	LOG_LEVEL: LogLevel;
	definitions: {
		global: DefinitionRepo;
	};
	triggerDefPreview: (el: HTMLElement) => void;
	settings: Settings;
	app: App;
}

// Initialise and inject globals
export function injectGlobals(
	settings: Settings,
	app: App,
	targetWindow: Window,
) {
	targetWindow.NoteDefinition = {
		app: app,
		LOG_LEVEL: activeWindow.NoteDefinition?.LOG_LEVEL || LogLevel.Error,
		definitions: {
			global: new DefinitionRepo(),
		},
		triggerDefPreview: (el: HTMLElement) => {
			const word = el.getAttr("def");
			if (!word) return;

			const def = getDefFileManager().get(word);
			if (!def) return;

			if (Platform.isMobile) {
				const defModal = getDefinitionModal();
				defModal.open(def);
				return;
			}

			const defPopover = getDefinitionPopover();
			let isOpen = false;

			if (el.onmouseenter) {
				const openPopover = setTimeout(() => {
					defPopover.openAtCoords(def, el.getBoundingClientRect());
					isOpen = true;
				}, 200);

				el.onmouseleave = () => {
					const popoverSettings = getSettings().defPopoverConfig;
					if (!isOpen) {
						clearTimeout(openPopover);
					} else if (
						popoverSettings.popoverDismissEvent ===
						PopoverDismissType.MouseExit
					) {
						defPopover.clickClose();
					}
				};
				return;
			}
			defPopover.openAtCoords(def, el.getBoundingClientRect());
		},
		settings,
	};
}


================================================
FILE: src/main.ts
================================================
import {
	Menu,
	Notice,
	Plugin,
	TFolder,
	WorkspaceWindow,
	TFile,
	MarkdownView,
} from "obsidian";
import { injectGlobals } from "./globals";
import { logDebug } from "./util/log";
import { definitionMarker } from "./editor/decoration";
import { Extension } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { DefManager, initDefFileManager } from "./core/def-file-manager";
import { Definition } from "./core/model";
import {
	getDefinitionPopover,
	initDefinitionPopover,
} from "./editor/definition-popover";
import { postProcessor } from "./editor/md-postprocessor";
import { DEFAULT_SETTINGS, getSettings, SettingsTab } from "./settings";
import { getMarkedWordUnderCursor } from "./util/editor";
import {
	FileExplorerDecoration,
	initFileExplorerDecoration,
} from "./ui/file-explorer";
import { EditDefinitionModal } from "./editor/edit-modal";
import { AddDefinitionModal } from "./editor/add-modal";
import { initDefinitionModal } from "./editor/mobile/definition-modal";
import { FMSuggestModal } from "./editor/frontmatter-suggest-modal";
import { registerDefFile } from "./editor/def-file-registration";
import { DefFileType } from "./core/file-type";

export default class NoteDefinition extends Plugin {
	activeEditorExtensions: Extension[] = [];
	defManager: DefManager;
	fileExplorerDeco: FileExplorerDecoration;

	async onload() {
		// Settings are injected into global object
		const settings = Object.assign(
			{},
			DEFAULT_SETTINGS,
			await this.loadData(),
		);
		injectGlobals(settings, this.app, window);

		this.registerEvent(
			this.app.workspace.on(
				"window-open",
				(win: WorkspaceWindow, newWindow: Window) => {
					injectGlobals(settings, this.app, newWindow);
				},
			),
		);

		logDebug("Load note definition plugin");

		initDefinitionPopover(this);
		initDefinitionModal(this.app);
		this.defManager = initDefFileManager(this.app);
		this.fileExplorerDeco = initFileExplorerDecoration(this.app);
		this.registerEditorExtension(this.activeEditorExtensions);
		this.updateEditorExts();

		this.registerCommands();
		this.registerEvents();

		this.addSettingTab(
			new SettingsTab(this.app, this, this.saveSettings.bind(this)),
		);
		this.registerMarkdownPostProcessor(postProcessor);

		this.fileExplorerDeco.run();
	}

	async saveSettings() {
		await this.saveData(window.NoteDefinition.settings);
		this.fileExplorerDeco.run();
		this.refreshDefinitions();
	}

	registerCommands() {
		this.addCommand({
			id: "preview-definition",
			name: "Preview definition",
			editorCallback: (editor) => {
				const curWord = getMarkedWordUnderCursor(editor);
				if (!curWord) return;
				const def =
					window.NoteDefinition.definitions.global.get(curWord);
				if (!def) return;
				getDefinitionPopover().openAtCursor(def);
			},
		});

		this.addCommand({
			id: "goto-definition",
			name: "Go to definition",
			editorCallback: (editor) => {
				const currWord = getMarkedWordUnderCursor(editor);
				if (!currWord) return;
				const def = this.defManager.get(currWord);
				if (!def) return;
				this.app.workspace.openLinkText(def.linkText, "");
			},
		});

		this.addCommand({
			id: "add-definition",
			name: "Add definition",
			editorCallback: (editor) => {
				const selectedText = editor.getSelection();
				const addModal = new AddDefinitionModal(this.app);
				addModal.open(selectedText);
			},
		});

		this.addCommand({
			id: "add-def-context",
			name: "Add definition context",
			editorCallback: (editor) => {
				const activeFile = this.app.workspace.getActiveFile();
				if (!activeFile) {
					new Notice(
						"Command must be used within an active opened file",
					);
					return;
				}
				const suggestModal = new FMSuggestModal(this.app, activeFile);
				suggestModal.open();
			},
		});

		this.addCommand({
			id: "refresh-definitions",
			name: "Refresh definitions",
			callback: () => {
				this.fileExplorerDeco.run();
				this.defManager.loadDefinitions();
			},
		});

		this.addCommand({
			id: "register-consolidated-def-file",
			name: "Register consolidated definition file",
			editorCallback: (_) => {
				const activeFile = this.app.workspace.getActiveFile();
				if (!activeFile) {
					new Notice(
						"Command must be used within an active opened file",
					);
					return;
				}
				registerDefFile(this.app, activeFile, DefFileType.Consolidated);
			},
		});

		this.addCommand({
			id: "register-atomic-def-file",
			name: "Register atomic definition file",
			editorCallback: (_) => {
				const activeFile = this.app.workspace.getActiveFile();
				if (!activeFile) {
					new Notice(
						"Command must be used within an active opened file",
					);
					return;
				}
				registerDefFile(this.app, activeFile, DefFileType.Atomic);
			},
		});
	}

	registerEvents() {
		this.registerEvent(
			this.app.workspace.on("active-leaf-change", async (leaf) => {
				if (!leaf) return;
				this.reloadUpdatedDefinitions();
				this.updateEditorExts();
				this.defManager.updateActiveFile();
			}),
		);

		this.registerEvent(
			this.app.workspace.on("editor-menu", (menu, editor) => {
				const defPopover = getDefinitionPopover();
				if (defPopover) {
					defPopover.close();
				}

				const curWord = getMarkedWordUnderCursor(editor);
				if (!curWord) {
					if (editor.getSelection()) {
						menu.addItem((item) => {
							item.setTitle("Add definition");
							item.setIcon("plus").onClick(() => {
								const addModal = new AddDefinitionModal(
									this.app,
								);
								addModal.open(editor.getSelection());
							});
						});
					}
					return;
				}
				const def = this.defManager.get(curWord);
				if (!def) {
					return;
				}
				this.registerMenuForMarkedWords(menu, def);
			}),
		);

		// Add file menu options
		this.registerEvent(
			this.app.workspace.on("file-menu", (menu, file, source) => {
				if (file instanceof TFolder) {
					menu.addItem((item) => {
						item.setTitle("Set definition folder")
							.setIcon("book-a")
							.onClick(() => {
								const settings = getSettings();
								settings.defFolder = file.path;
								this.saveSettings();
							});
					});
				}
			}),
		);

		// Creating files under def folder should register file as definition file
		this.registerEvent(
			this.app.vault.on("create", (file) => {
				const settings = getSettings();
				if (file.path.startsWith(settings.defFolder)) {
					this.fileExplorerDeco.run();
					this.refreshDefinitions();
				}
			}),
		);

		this.registerEvent(
			this.app.metadataCache.on("changed", (file: TFile) => {
				const currFile = this.app.workspace.getActiveFile();

				if (currFile && currFile.path === file.path) {
					this.defManager.updateActiveFile();

					let activeView =
						this.app.workspace.getActiveViewOfType(MarkdownView);

					if (activeView) {
						// @ts-expect-error, not typed
						const view = activeView.editor.cm as EditorView;
						const plugin = view.plugin(definitionMarker);

						if (plugin) {
							plugin.forceUpdate();
						}
					}
				}
			}),
		);
	}

	registerMenuForMarkedWords(menu: Menu, def: Definition) {
		menu.addItem((item) => {
			item.setTitle("Go to definition")
				.setIcon("arrow-left-from-line")
				.onClick(() => {
					this.app.workspace.openLinkText(def.linkText, "");
				});
		});

		menu.addItem((item) => {
			item.setTitle("Edit definition")
				.setIcon("pencil")
				.onClick(() => {
					const editModal = new EditDefinitionModal(this.app);
					editModal.open(def);
				});
		});
	}

	refreshDefinitions() {
		this.defManager.loadDefinitions();
	}

	reloadUpdatedDefinitions() {
		this.defManager.loadUpdatedFiles();
	}

	updateEditorExts() {
		const currFile = this.app.workspace.getActiveFile();
		if (currFile && this.defManager.isDefFile(currFile)) {
			// TODO: Editor extension for definition file
			this.setActiveEditorExtensions([]);
		} else {
			this.setActiveEditorExtensions(definitionMarker);
		}
	}

	private setActiveEditorExtensions(...ext: Extension[]) {
		this.activeEditorExtensions.length = 0;
		this.activeEditorExtensions.push(...ext);
		this.app.workspace.updateOptions();
	}

	onunload() {
		logDebug("Unload note definition plugin");
		getDefinitionPopover().cleanUp();
	}
}


================================================
FILE: src/settings.ts
================================================
import {
	App,
	Modal,
	Notice,
	Plugin,
	PluginSettingTab,
	Setting,
	setTooltip,
} from "obsidian";
import { DefFileType } from "./core/file-type";

export enum PopoverEventSettings {
	Hover = "hover",
	Click = "click",
}

export enum PopoverDismissType {
	Click = "click",
	MouseExit = "mouse_exit",
}

export interface DividerSettings {
	dash: boolean;
	underscore: boolean;
}

export interface DefFileParseConfig {
	defaultFileType: DefFileType;
	divider: DividerSettings;
	autoPlurals: boolean;
	enableCaseSensitive: boolean;
}

export interface DefinitionPopoverConfig {
	displayAliases: boolean;
	displayDefFileName: boolean;
	enableCustomSize: boolean;
	maxWidth: number;
	maxHeight: number;
	popoverDismissEvent: PopoverDismissType;
	enableDefinitionLink: boolean;
	backgroundColour?: string;
}

export interface Settings {
	enableInReadingView: boolean;
	enableOnLinks: boolean;
	enableSpellcheck: boolean;
	defFolder: string;
	popoverEvent: PopoverEventSettings;
	defFileParseConfig: DefFileParseConfig;
	defPopoverConfig: DefinitionPopoverConfig;
}

export const VALID_DEFINITION_FILE_TYPES = [".md"];

export const DEFAULT_DEF_FOLDER = "definitions";

export const DEFAULT_SETTINGS: Partial<Settings> = {
	enableInReadingView: true,
	enableOnLinks: true,
	enableSpellcheck: true,
	popoverEvent: PopoverEventSettings.Hover,
	defFileParseConfig: {
		defaultFileType: DefFileType.Consolidated,
		divider: {
			dash: true,
			underscore: false,
		},
		autoPlurals: false,
		enableCaseSensitive: false,
	},
	defPopoverConfig: {
		displayAliases: true,
		displayDefFileName: false,
		enableCustomSize: false,
		maxWidth: 150,
		maxHeight: 150,
		popoverDismissEvent: PopoverDismissType.Click,
		enableDefinitionLink: false,
	},
};

export class SettingsTab extends PluginSettingTab {
	plugin: Plugin;
	settings: Settings;
	saveCallback: () => Promise<void>;

	constructor(app: App, plugin: Plugin, saveCallback: () => Promise<void>) {
		super(app, plugin);
		this.plugin = plugin;
		this.settings = window.NoteDefinition.settings;
		this.saveCallback = saveCallback;
	}

	display(): void {
		let { containerEl } = this;

		containerEl.empty();

		new Setting(containerEl)
			.setName("Enable in Reading View")
			.setDesc(
				"Allow defined phrases and definition popovers to be shown in Reading View",
			)
			.addToggle((component) => {
				component.setValue(this.settings.enableInReadingView);
				component.onChange(async (val) => {
					this.settings.enableInReadingView = val;
					await this.saveCallback();
					this.display();
				});
			});

		if (this.settings.enableInReadingView) {
			new Setting(containerEl)
				.setName("Enable highlight on links")
				.setDesc(
					"Allow defined phrases and definition popovers to display on links (only applies to Reading View)",
				)
				.addToggle((component) => {
					component.setValue(this.settings.enableOnLinks);
					component.onChange(async (val) => {
						this.settings.enableOnLinks = val;
						await this.saveCallback();
					});
				});
		}

		new Setting(containerEl)
			.setName("Enable spellcheck for defined words")
			.setDesc("Allow defined words and phrases to be spellchecked")
			.addToggle((component) => {
				component.setValue(this.settings.enableSpellcheck);
				component.onChange(async (val) => {
					this.settings.enableSpellcheck = val;
					await this.saveCallback();
				});
			});

		new Setting(containerEl)
			.setName("Enable Case Sensitivity")
			.setDesc("Only match if the cases of both terms match")
			.addToggle((component) => {
				component.setValue(this.settings.defFileParseConfig.enableCaseSensitive);
				component.onChange(async (val) => {
					this.settings.defFileParseConfig.enableCaseSensitive = val;
					await this.saveCallback();
				});
			});

		new Setting(containerEl)
			.setName("Definitions folder")
			.setDesc(
				"Files within this folder will be parsed to register definitions",
			)
			.addText((component) => {
				component.setValue(this.settings.defFolder);
				component.setPlaceholder(DEFAULT_DEF_FOLDER);
				component.setDisabled(true);
				setTooltip(
					component.inputEl,
					"In the file explorer, right-click on the desired folder and click on 'Set definition folder' to change the definition folder",
					{
						delay: 100,
					},
				);
			});

		new Setting(containerEl)
			.setName("Definition file format settings")
			.setDesc("Customise parsing rules for definition files")
			.addExtraButton((component) => {
				component.onClick(() => {
					const modal = new Modal(this.app);
					modal.setTitle("Definition file format settings");
					new Setting(modal.contentEl)
						.setName("Divider")
						.setHeading();
					new Setting(modal.contentEl)
						.setName("Dash")
						.setDesc("Use triple dash (---) as divider")
						.addToggle((component) => {
							component.setValue(
								this.settings.defFileParseConfig.divider.dash,
							);
							component.onChange(async (value) => {
								if (
									!value &&
									!this.settings.defFileParseConfig.divider
										.underscore
								) {
									new Notice(
										"At least one divider must be chosen",
										2000,
									);
									component.setValue(
										this.settings.defFileParseConfig.divider
											.dash,
									);
									return;
								}
								this.settings.defFileParseConfig.divider.dash =
									value;
								await this.saveCallback();
							});
						});
					new Setting(modal.contentEl)
						.setName("Underscore")
						.setDesc("Use triple underscore (___) as divider")
						.addToggle((component) => {
							component.setValue(
								this.settings.defFileParseConfig.divider
									.underscore,
							);
							component.onChange(async (value) => {
								if (
									!value &&
									!this.settings.defFileParseConfig.divider
										.dash
								) {
									new Notice(
										"At least one divider must be chosen",
										2000,
									);
									component.setValue(
										this.settings.defFileParseConfig.divider
											.underscore,
									);
									return;
								}
								this.settings.defFileParseConfig.divider.underscore =
									value;
								await this.saveCallback();
							});
						});
					modal.open();
				});
			});

		new Setting(containerEl)
			.setName("Default definition file type")
			.setDesc(
				"When the 'def-type' frontmatter is not specified, the definition file will be treated as this configured default file type.",
			)
			.addDropdown((component) => {
				component.addOption(DefFileType.Consolidated, "consolidated");
				component.addOption(DefFileType.Atomic, "atomic");
				component.setValue(
					this.settings.defFileParseConfig.defaultFileType ??
						DefFileType.Consolidated,
				);
				component.onChange(async (val) => {
					this.settings.defFileParseConfig.defaultFileType =
						val as DefFileType;
					await this.saveCallback();
				});
			});

		new Setting(containerEl)
			.setName("Automatically detect plurals -- English only")
			.setDesc(
				"Attempt to automatically generate aliases for words using English pluralisation rules",
			)
			.addToggle((component) => {
				component.setValue(
					this.settings.defFileParseConfig.autoPlurals,
				);
				component.onChange(async (val) => {
					this.settings.defFileParseConfig.autoPlurals = val;
					await this.saveCallback();
				});
			});

		new Setting(containerEl)
			.setHeading()
			.setName("Definition Popover Settings");

		new Setting(containerEl)
			.setName("Definition popover display event")
			.setDesc(
				"Choose the trigger event for displaying the definition popover",
			)
			.addDropdown((component) => {
				component.addOption(PopoverEventSettings.Hover, "Hover");
				component.addOption(PopoverEventSettings.Click, "Click");
				component.setValue(this.settings.popoverEvent);
				component.onChange(async (value) => {
					if (
						value === PopoverEventSettings.Hover ||
						value === PopoverEventSettings.Click
					) {
						this.settings.popoverEvent = value;
					}
					if (
						this.settings.popoverEvent ===
						PopoverEventSettings.Click
					) {
						this.settings.defPopoverConfig.popoverDismissEvent =
							PopoverDismissType.Click;
					}
					await this.saveCallback();
					this.display();
				});
			});

		if (this.settings.popoverEvent === PopoverEventSettings.Hover) {
			new Setting(containerEl)
				.setName("Definition popover dismiss event")
				.setDesc(
					"Configure the manner in which you would like to close/dismiss the definition popover.",
				)
				.addDropdown((component) => {
					component.addOption(PopoverDismissType.Click, "Click");
					component.addOption(
						PopoverDismissType.MouseExit,
						"Mouse exit",
					);
					if (!this.settings.defPopoverConfig.popoverDismissEvent) {
						this.settings.defPopoverConfig.popoverDismissEvent =
							PopoverDismissType.Click;
						this.saveCallback();
					}
					component.setValue(
						this.settings.defPopoverConfig.popoverDismissEvent,
					);
					component.onChange(async (value) => {
						if (
							value === PopoverDismissType.MouseExit ||
							value === PopoverDismissType.Click
						) {
							this.settings.defPopoverConfig.popoverDismissEvent =
								value;
						}
						await this.saveCallback();
					});
				});
		}

		new Setting(containerEl)
			.setName("Display aliases")
			.setDesc(
				"Display the list of aliases configured for the definition",
			)
			.addToggle((component) => {
				component.setValue(
					this.settings.defPopoverConfig.displayAliases,
				);
				component.onChange(async (value) => {
					this.settings.defPopoverConfig.displayAliases = value;
					await this.saveCallback();
				});
			});

		new Setting(containerEl)
			.setName("Display definition source file")
			.setDesc("Display the title of the definition's source file")
			.addToggle((component) => {
				component.setValue(
					this.settings.defPopoverConfig.displayDefFileName,
				);
				component.onChange(async (value) => {
					this.settings.defPopoverConfig.displayDefFileName = value;
					await this.saveCallback();
				});
			});

		new Setting(containerEl)
			.setName("Custom popover size")
			.setDesc(
				"Customise the maximum popover size. This is not recommended as it prevents dynamic sizing of the popover based on your viewport.",
			)
			.addToggle((component) => {
				component.setValue(
					this.settings.defPopoverConfig.enableCustomSize,
				);
				component.onChange(async (value) => {
					this.settings.defPopoverConfig.enableCustomSize = value;
					await this.saveCallback();
					this.display();
				});
			});

		if (this.settings.defPopoverConfig.enableCustomSize) {
			new Setting(containerEl)
				.setName("Popover width (px)")
				.setDesc("Maximum width of the definition popover")
				.addSlider((component) => {
					component.setLimits(150, window.innerWidth, 1);
					component.setValue(this.settings.defPopoverConfig.maxWidth);
					component.setDynamicTooltip();
					component.onChange(async (val) => {
						this.settings.defPopoverConfig.maxWidth = val;
						await this.saveCallback();
					});
				});

			new Setting(containerEl)
				.setName("Popover height (px)")
				.setDesc("Maximum height of the definition popover")
				.addSlider((component) => {
					component.setLimits(150, window.innerHeight, 1);
					component.setValue(
						this.settings.defPopoverConfig.maxHeight,
					);
					component.setDynamicTooltip();
					component.onChange(async (val) => {
						this.settings.defPopoverConfig.maxHeight = val;
						await this.saveCallback();
					});
				});
		}

		new Setting(containerEl)
			.setName("Enable definition links")
			.setDesc(
				"Definitions within popovers will be marked and can be clicked to go to definition.",
			)
			.addToggle((component) => {
				component.setValue(
					this.settings.defPopoverConfig.enableDefinitionLink,
				);
				component.onChange(async (val) => {
					this.settings.defPopoverConfig.enableDefinitionLink = val;
					await this.saveCallback();
				});
			});

		new Setting(containerEl)
			.setName("Background colour")
			.setDesc(
				"Customise the background colour of the definition popover",
			)
			.addExtraButton((component) => {
				component.setIcon("rotate-ccw");
				component.setTooltip("Reset to default colour set by theme");
				component.onClick(async () => {
					this.settings.defPopoverConfig.backgroundColour = undefined;
					await this.saveCallback();
					this.display();
				});
			})
			.addColorPicker((component) => {
				if (this.settings.defPopoverConfig.backgroundColour) {
					component.setValue(
						this.settings.defPopoverConfig.backgroundColour,
					);
				}
				component.onChange(async (val) => {
					this.settings.defPopoverConfig.backgroundColour = val;
					await this.saveCallback();
				});
			});
	}
}

export function getSettings(): Settings {
	return window.NoteDefinition.settings;
}


================================================
FILE: src/tests/consolidated-def-parser.test.ts
================================================
import { App, TFile } from "obsidian";
import { ConsolidatedDefParser } from "src/core/consolidated-def-parser";
import { DefFileType } from "src/core/file-type";
import { DefFileParseConfig } from "src/settings";

const fs = require("node:fs");

// Setup for test file
const consolidatedDefData = fs.readFileSync(
	"src/tests/def-file-samples/consolidated-definitions-test.md",
	"utf8",
);

const caseSensitiveDefData = fs.readFileSync(
	"src/tests/def-file-samples/case-sensitve-definitions-test.md",
	"utf8",
);

const consolidatedTrainingWhitespace = fs.readFileSync(
	"src/tests/def-file-samples/consolidated-trailing-whitespace.md",
	"utf8",
);

const consolidatedStartFileWhitespace = fs.readFileSync(
	"src/tests/def-file-samples/consolidated-start-of-file-whitespace.md",
	"utf8",
);

const parseSettings: DefFileParseConfig = {
	defaultFileType: DefFileType.Consolidated,
	divider: {
		underscore: true,
		dash: true,
	},
	autoPlurals: false,
	enableCaseSensitive: false,
};

const caseSensitiveParseSettings: DefFileParseConfig = {
	defaultFileType: DefFileType.Consolidated,
	divider: {
		underscore: true,
		dash: true,
	},
	autoPlurals: false,
	enableCaseSensitive: true,
};

const file = {
	path: "src/tests/consolidated-definitions-test.md",
};

const parser = new ConsolidatedDefParser(
	null as unknown as App,
	file as TFile,
	parseSettings,
);

const caseSensitiveParser = new ConsolidatedDefParser(
	null as unknown as App,
	file as TFile,
	caseSensitiveParseSettings,
);

const definitions = parser.directParseFile(consolidatedDefData);
const caseSensitiveDefinitions =
	caseSensitiveParser.directParseFile(caseSensitiveDefData);

describe("Valid definition file can be parsed correctly", () => {
	it("Words of definitions are parsed correctly", async () => {
		expect(definitions.find((def) => def.word === "First")).toBeDefined();
		expect(
			definitions.find((def) => def.word === "Multiple-word definition"),
		).toBeDefined();
		expect(
			definitions.find((def) => def.word === "Alias definition"),
		).toBeDefined();
		expect(
			definitions.find((def) => def.word === "Markdown support"),
		).toBeDefined();
	});

	it("Keys are stored as lowercase of words when case-sensitve disabled", () => {
		expect(definitions.find((def) => def.word === "First")?.key).toBe(
			"first",
		);
		expect(
			definitions.find((def) => def.word === "Multiple-word definition")
				?.key,
		).toBe("multiple-word definition");
		expect(
			definitions.find((def) => def.word === "Alias definition")?.key,
		).toBe("alias definition");
		expect(
			definitions.find((def) => def.word === "Markdown support")?.key,
		).toBe("markdown support");
	});

	it("Definitions are parsed correctly", () => {
		expect(definitions.find((def) => def.key === "first")?.definition).toBe(
			"This is the first definition to test basic functionality.",
		);
		expect(
			definitions.find((def) => def.key === "multiple-word definition")
				?.definition,
		).toBe("This ensures that multiple-word definitions works.");
		expect(
			definitions.find((def) => def.key === "alias definition")
				?.definition,
		).toBe("This tests if the alias function works.");
		expect(
			definitions.find((def) => def.key === "markdown support")
				?.definition,
		).toBe("Markdown syntax _should_ *work*.");
	});

	it("Positions are parsed correctly", () => {
		expect(
			definitions.find((def) => def.key === "first")?.position?.from,
		).toBe(0);
		expect(
			definitions.find((def) => def.key === "first")?.position?.to,
		).toBe(4);
		expect(
			definitions.find((def) => def.key === "multiple-word definition")
				?.position?.from,
		).toBe(6);
		expect(
			definitions.find((def) => def.key === "multiple-word definition")
				?.position?.to,
		).toBe(10);
		expect(
			definitions.find((def) => def.key === "alias definition")?.position
				?.from,
		).toBe(12);
		expect(
			definitions.find((def) => def.key === "alias definition")?.position
				?.to,
		).toBe(18);
		expect(
			definitions.find((def) => def.key === "markdown support")?.position
				?.from,
		).toBe(20);
		expect(
			definitions.find((def) => def.key === "markdown support")?.position
				?.to,
		).toBe(22);
	});

	it("Aliases are parsed correctly", () => {
		expect(
			definitions.find((def) => def.key === "alias definition")?.aliases,
		).toStrictEqual(["Alias1", "Alias2"]);
	});
});

describe("Consolidated definition file has odd formatting, but still valid syntax", () => {
	it("Extra end of file whitespace characters should be ignored", () => {
		const file = {
			path: "src/tests/consolidated-trailing-whitespace.md",
		};
		const parser = new ConsolidatedDefParser(
			null as unknown as App,
			file as TFile,
			parseSettings,
		);

		const definitions = parser.directParseFile(
			consolidatedTrainingWhitespace,
		);
		expect(definitions.find((def) => def.word === "First")).toBeDefined();
		expect(
			definitions.find((def) => def.word === "Multiple-word definition"),
		).toBeDefined();
		expect(
			definitions.find((def) => def.word === "Alias definition"),
		).toBeDefined();
		expect(
			definitions.find((def) => def.word === "Markdown support"),
		).toBeDefined();
	});

	it("Start of file whitespace should be ignored", () => {
		const file = {
			path: "src/tests/consolidated-start-of-file-whitespace.md",
		};
		const parser = new ConsolidatedDefParser(
			null as unknown as App,
			file as TFile,
			parseSettings,
		);
		const definitions = parser.directParseFile(
			consolidatedStartFileWhitespace,
		);
		expect(definitions.find((def) => def.word === "First")).toBeDefined();
		expect(
			definitions.find((def) => def.word === "Multiple-word definition"),
		).toBeDefined();
		expect(
			definitions.find((def) => def.word === "Alias definition"),
		).toBeDefined();
		expect(
			definitions.find((def) => def.word === "Markdown support"),
		).toBeDefined();
	});
});

describe("Valid definition file can be parsed correctly when case-sensitive enabled", () => {
	it("Keys are stored with correct case when case-sensitive enabled", () => {
		expect(
			caseSensitiveDefinitions.find((def) => def.word === "First")?.key,
		).toBe("First");
		expect(
			caseSensitiveDefinitions.find((def) => def.word === "first")?.key,
		).toBe("first");
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.word === "Multiple-word definition",
			)?.key,
		).toBe("Multiple-word definition");
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.word === "Multiple-word Definition",
			)?.key,
		).toBe("Multiple-word Definition");
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.word === "Alias definition",
			)?.key,
		).toBe("Alias definition");
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.word === "alias definition",
			)?.key,
		).toBeUndefined;
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.word === "Markdown support",
			)?.key,
		).toBe("Markdown support");
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.word === "markdown support",
			)?.key,
		).toBeUndefined;
	});

	it("Definitions are parsed correctly when case-sensitive enabled", () => {
		expect(
			caseSensitiveDefinitions.find((def) => def.key === "first")
				?.definition,
		).toBe("This is the first definition to test basic functionality.");
		expect(
			caseSensitiveDefinitions.find((def) => def.key === "First")
				?.definition,
		).toBe("This is a different definition than the first.");
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.key === "Multiple-word definition",
			)?.definition,
		).toBe("This ensures that multiple-word definitions works.");
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.key === "Multiple-word Definition",
			)?.definition,
		).toBe("This ensures that case matters in multiple word definitions.");
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.key === "Alias definition",
			)?.definition,
		).toBe("This tests if the alias function works.");
		expect(
			caseSensitiveDefinitions.find(
				(def) => def.key === "Markdown support",
			)?.definition,
		).toBe("Markdown syntax _should_ *work*.");
	});
});


================================================
FILE: src/tests/decorator.test.ts
================================================
import { scanText } from "src/editor/decoration";
import { PhraseInfo } from "src/editor/definition-search";
import { PTreeNode } from "src/editor/prefix-tree";

jest.mock("src/settings", () => ({
	getSettings: jest.fn(() => ({
		defFileParseConfig: {
			enableCaseSensitive: false,
		},
	})),
}));
afterEach(() => {
	jest.clearAllMocks();
});

const pTree = new PTreeNode();
pTree.add("word1");
pTree.add("word2");
pTree.add("a phrase");
pTree.add("a long phrase");
pTree.add("long");

test("Defined words are correctly detected in a simple sentence", () => {
	const text =
		"Hi this is a simple sentence with word1, word2 and a phrase defined.";
	const phraseInfo = scanText(text, 0, pTree);
	const expectedPhraseInfo: PhraseInfo[] = [
		{
			from: 34,
			to: 39,
			phrase: "word1",
		},
		{
			from: 41,
			to: 46,
			phrase: "word2",
		},
		{
			from: 51,
			to: 59,
			phrase: "a phrase",
		},
	];
	expect(phraseInfo).toStrictEqual(expectedPhraseInfo);
});

test("Defined words are correctly detected in a paragraph", () => {
	const text = `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. 
Ut enim ad minim veniam, word1 quis nostrud exercitation ullamco laboris nisi word2 ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in a phrase voluptate velit esse cillum dolore eu fugiat nulla pariatur. 
Excepteur sint occaecat cupidatat non proident, sunt word2 in culpa qui officia deserunt mollit anim id word1 est laborum`;

	const phraseInfo = scanText(text, 0, pTree);
	const expectedPhraseInfo = [
		{ phrase: "word1", from: 150, to: 155 },
		{ phrase: "word2", from: 203, to: 208 },
		{ phrase: "a phrase", from: 287, to: 295 },
		{ phrase: "word2", from: 411, to: 416 },
		{ phrase: "word1", from: 462, to: 467 },
	];

	expect(phraseInfo).toStrictEqual(expectedPhraseInfo);
});

test("Offset is correctly added to positions", () => {
	const text =
		"Hi this is a simple sentence with word1, word2 and a phrase defined.";
	const phraseInfo = scanText(text, 2, pTree);
	const expectedPhraseInfo: PhraseInfo[] = [
		{
			from: 36,
			to: 41,
			phrase: "word1",
		},
		{
			from: 43,
			to: 48,
			phrase: "word2",
		},
		{
			from: 53,
			to: 61,
			phrase: "a phrase",
		},
	];
	expect(phraseInfo).toStrictEqual(expectedPhraseInfo);
});

test("Definitions that are a subset of another are detected correctly. The longer definition is preferred.", () => {
	const text =
		"Although longer definitions are preferred in a long phrase. The long word should still be normally detected";
	const phraseInfo = scanText(text, 0, pTree);
	const expectedPhraseInfo = [
		{ phrase: "a long phrase", from: 45, to: 58 },
		{ phrase: "long", from: 64, to: 68 },
	];
	expect(phraseInfo).toStrictEqual(expectedPhraseInfo);
});


================================================
FILE: src/tests/def-file-samples/case-sensitve-definitions-test.md
================================================
# first

This is the first definition to test basic functionality.

---

# First

This is a different definition than the first.

---

# Multiple-word definition

This ensures that multiple-word definitions works.

---

# Multiple-word Definition

This ensures that case matters in multiple word definitions.

---

# Alias definition

*Alias1, alias2*

This tests if the alias function works.

---

# Markdown support

Markdown syntax _should_ *work*.


================================================
FILE: src/tests/def-file-samples/consolidated-definitions-test.md
================================================
# First

This is the first definition to test basic functionality.

---

# Multiple-word definition

This ensures that multiple-word definitions works.

---

# Alias definition

*Alias1, Alias2*

This tests if the alias function works.

---

# Markdown support

Markdown syntax _should_ *work*.


================================================
FILE: src/tests/def-file-samples/consolidated-start-of-file-whitespace.md
================================================






 
 # First

This is the first definition to test basic functionality.

---

# Multiple-word definition

This ensures that multiple-word definitions works.

---

# Alias definition

*Alias1, Alias2*

This tests if the alias function works.

---

# Markdown support

Markdown syntax _should_ *work*.



================================================
FILE: src/tests/def-file-samples/consolidated-trailing-delimiter.md
================================================
# First

This is the first definition to test basic functionality.

---

# Multiple-word definition

This ensures that multiple-word definitions works.

---

# Alias definition

*Alias1, Alias2*

This tests if the alias function works.

---

# Markdown support

Markdown syntax _should_ *work*.

   
--- 



================================================
FILE: src/tests/def-file-samples/consolidated-trailing-whitespace.md
================================================
# First

This is the first definition to test basic functionality.

---

# Multiple-word definition

This ensures that multiple-word definitions works.

---

# Alias definition

*Alias1, Alias2*

This tests if the alias function works.

---

# Markdown support

Markdown syntax _should_ *work*.

   


================================================
FILE: src/tests/def-file-updater.test.ts
================================================
import { App, TFile } from "obsidian";
import { DefFileUpdater } from "src/core/def-file-updater";
import { DefFileType } from "src/core/file-type";
import { DefManager } from "__mocks__/internals";

jest.mock("src/core/def-file-manager", () => {
	return {
		getDefFileManager: () => new DefManager(),
	};
});

jest.mock("src/settings", () => ({
	getSettings: jest.fn(() => ({
		defFileParseConfig: {
			divider: {
				dash: true,
				underscore: false,
			},
		},
	})),
}));

jest.mock("src/util/log");

const app = new App();
const defFileUpdater = new DefFileUpdater(app);

const vaultModify = jest.spyOn(app.vault, "modify");

afterEach(() => {
	jest.clearAllMocks();
});

test("Update atomic definition", async () => {
	const file = {
		basename: "atomic",
		extension: "md",
	} as TFile;
	await defFileUpdater.updateDefinition({
		key: "atomic",
		word: "atomic",
		aliases: [],
		definition: "this is a test definition",
		file: file,
		linkText: "",
		fileType: DefFileType.Atomic,
	});
	expect(vaultModify).toHaveBeenCalledWith(file, "this is a test definition");
});

describe("Test modifying consolidated file", () => {
	it("Update consolidated definition", async () => {
		const file = {
			basename: "consolidated",
			extension: "md",
		} as TFile;

		const oldContent = `# oldWord

*oldAlias*

oldDefinition

---

# Another Definition

anotherDefValue

---

# Yet another def

Yet another definition`;
		const newDefinitionText = "This is a definition, blah blah blah.";
		const expectedNewContent = `# oldWord

*oldAlias*

This is a definition, blah blah blah.

---

# Another Definition

anotherDefValue

---

# Yet another def

Yet another definition`;

		jest.spyOn(app.vault, "read").mockResolvedValue(oldContent);
		jest.spyOn(app.metadataCache, "getFileCache").mockReturnValue({});

		await defFileUpdater.updateDefinition({
			key: "oldword",
			word: "oldWord",
			aliases: ["oldAlias"],
			definition: newDefinitionText,
			file: file,
			linkText: "",
			fileType: DefFileType.Consolidated,
		});

		expect(vaultModify).toHaveBeenCalledWith(file, expectedNewContent);
	});

	it("Add definition to consolidated file", async () => {
		const file = {
			basename: "consolidated",
			extension: "md",
		} as TFile;

		const oldContent = `---
def-type: consolidated
---

# Existing Def
Existing definition.
`;
		const newDef = {
			word: "New Def",
			aliases: ["New Alias"],
			definition: "This is a new definition.",
			file: file,
			fileType: DefFileType.Consolidated,
		};
		const expectedNewContent = `---
def-type: consolidated
---
# Existing Def

Existing definition.

---

# New Def

*New Alias*

This is a new definition.`;

		jest.spyOn(app.vault, "read").mockResolvedValue(oldContent);
		jest.spyOn(app.metadataCache, "getFileCache").mockReturnValue({
			frontmatterPosition: {
				start: {
					line: 0,
					col: 0,
					offset: 0,
				},
				end: {
					line: 2,
					col: 3,
					offset: 30,
				},
			},
		});

		await defFileUpdater.addDefinition(newDef);

		expect(vaultModify).toHaveBeenCalledWith(file, expectedNewContent);
	});

	it("Add definition to empty file", async () => {
		const file = {
			basename: "consolidated",
			extension: "md",
		} as TFile;

		const oldContent = ``;
		const newDef = {
			word: "New Def",
			aliases: ["New Alias"],
			definition: "This is a new definition.",
			file: file,
			fileType: DefFileType.Consolidated,
		};
		const expectedNewContent = `# New Def

*New Alias*

This is a new definition.`;

		jest.spyOn(app.vault, "read").mockResolvedValue(oldContent);

		await defFileUpdater.addDefinition(newDef);

		expect(vaultModify).toHaveBeenCalledWith(file, expectedNewContent);
	});
});


================================================
FILE: src/types/obsidian.d.ts
================================================
import { View } from "obsidian";

interface FileExplorerView extends View {
	fileItems: { [key: string]: FileItem };
}

interface FileItem {
	selfEl: HTMLElement;
	innerEl: HTMLElement;
}


================================================
FILE: src/ui/file-explorer.ts
================================================
import { App } from "obsidian";
import {
	DEFAULT_DEF_FOLDER,
	getSettings,
	VALID_DEFINITION_FILE_TYPES,
} from "src/settings";
import { FileExplorerView } from "src/types/obsidian";
import { logDebug } from "src/util/log";

let fileExplorerDecoration: FileExplorerDecoration;

const MAX_RETRY = 3;
const RETRY_INTERVAL = 1000;
const DIV_ID = "def-tag-id";

export class FileExplorerDecoration {
	app: App;
	retryCount: number;

	constructor(app: App) {
		this.app = app;
	}

	// Take note: May not be re-entrant
	async run() {
		this.retryCount = 0;

		// Retry required as some views may not be loaded on initial app start
		while (this.retryCount < MAX_RETRY) {
			try {
				this.exec();
			} catch (e) {
				logDebug(e);
				this.retryCount++;
				await sleep(RETRY_INTERVAL);
				continue;
			}
			return;
		}
	}

	private exec() {
		const fileExplorer =
			this.app.workspace.getLeavesOfType("file-explorer")[0];
		if (!fileExplorer) {
			// This is an expected behaviour, likely due to
			throw new Error(
				"app.workspace.getLeavesOfType('file-explorer') returned undefined (file explorer may not be available in view yet)",
			);
		}
		const fileExpView = fileExplorer.view as FileExplorerView;

		const settings = getSettings();
		Object.keys(fileExpView.fileItems).forEach((k) => {
			const fileItem = fileExpView.fileItems[k];

			// Clear previously added ones (if exist)
			const fileTags =
				fileItem.selfEl.getElementsByClassName("nav-file-tag");
			for (let i = 0; i < fileTags.length; i++) {
				const fileTag = fileTags[i];
				if (fileTag.id === DIV_ID) {
					fileTag.remove();
				}
			}

			const defFolder = settings.defFolder || DEFAULT_DEF_FOLDER;

			// If def folder is an invalid folder path, then do not add any tags
			if (!fileExpView.fileItems[defFolder]) {
				return;
			}

			if (
				k.startsWith(defFolder) &&
				VALID_DEFINITION_FILE_TYPES.some((ext) => k.endsWith(ext))
			) {
				this.tagFile(fileExpView, k, "DEF");
			}
		});
	}

	private tagFile(
		explorer: FileExplorerView,
		filePath: string,
		tagContent: string,
	) {
		const el = explorer.fileItems[filePath];
		if (!el) {
			logDebug(`No file item with filepath ${filePath} found`);
			return;
		}

		const fileTags = el.selfEl.getElementsByClassName("nav-file-tag");
		for (let i = 0; i < fileTags.length; i++) {
			const fileTag = fileTags[i];
			fileTag.remove();
		}

		el.selfEl.createDiv({
			cls: "nav-file-tag",
			text: tagContent,
			attr: {
				id: DIV_ID,
			},
		});
	}
}

export function initFileExplorerDecoration(app: App): FileExplorerDecoration {
	fileExplorerDecoration = new FileExplorerDecoration(app);
	return fileExplorerDecoration;
}

export function getFileExplorerDecoration(app: App): FileExplorerDecoration {
	if (fileExplorerDecoration) {
		return fileExplorerDecoration;
	}
	return initFileExplorerDecoration(app);
}


================================================
FILE: src/util/editor.ts
================================================
import { Editor } from "obsidian";
import { getMarkedPhrases } from "src/editor/decoration";
import { getSettings } from "src/settings";


export function getMarkedWordUnderCursor(editor: Editor) {
	const currWord = getWordByOffset(editor.posToOffset(editor.getCursor()));
	return normaliseWord(currWord);
}

export function normaliseWord(word: string) {
	if (getSettings().defFileParseConfig.enableCaseSensitive)
		return word.trimStart().trimEnd();
	else
		return word.trimStart().trimEnd().toLowerCase();
}

function getWordByOffset(offset: number): string {
	const markedPhrases = getMarkedPhrases();
	let start = 0;
	let end = markedPhrases.length - 1;

	// Binary search to get marked word at provided position
	while (start <= end) {
		let mid = Math.floor((start + end) / 2);

		let currPhrase = markedPhrases[mid];
		if (offset >= currPhrase.from && offset <= currPhrase.to) {
			return currPhrase.phrase;
		}
		if (offset < currPhrase.from) {
			end = mid - 1;
		}
		if (offset > currPhrase.to) {
			start = mid + 1;
		}
	}
	return "";
}


================================================
FILE: src/util/log.ts
================================================
// Rudimentary logger implementation

export enum LogLevel {
	Silent,
	Error,
	Warn,
	Info,
	Debug,
}

const levelMap = {
	0: "SILENT", // Should not be used
	1: "ERROR",
	2: "WARN",
	3: "INFO",
	4: "DEBUG",
};

// Log only if current log level is >= specified log level
function logWithLevel(msg: string, logLevel: LogLevel) {
	if (window.NoteDefinition.LOG_LEVEL >= logLevel) {
		console.log(`${levelMap[logLevel]}: ${msg}`);
	}
}

// Convenience methods for each level

export function logDebug(msg: string) {
	logWithLevel(msg, LogLevel.Debug);
}

export function logInfo(msg: string) {
	logWithLevel(msg, LogLevel.Info);
}

export function logWarn(msg: string) {
	logWithLevel(msg, LogLevel.Warn);
}

export function logError(msg: string) {
	logWithLevel(msg, LogLevel.Error);
}


================================================
FILE: src/util/retry.ts
================================================
const RETRY_INTERVAL = 1000;

export function useRetry(retryCount?: number) {
	let shouldRetry = false;
	let maxRetries = retryCount ?? 3;
	let currRetry = 0;

	async function exec(func: any) {
		while (currRetry < maxRetries) {
			const output = func();
			if (!shouldRetry) {
				return output;
			}
			shouldRetry = false;
			currRetry++;
			await sleep(RETRY_INTERVAL);
		}
		throw new Error("Failed to exec function, hit max retries");
	}

	function setShouldRetry() {
		shouldRetry = true;
	}

	return {
		exec,
		setShouldRetry,
	};
}


================================================
FILE: styles.css
================================================
.definition-popover {
	background-color: var(--background-secondary);
	border: 1px solid var(--background-modifier-border-hover);
	border-radius: var(--radius-m);
	position: absolute;
	padding: var(--size-4-2) var(--size-4-3);
	box-shadow: var(--shadow-s);
	min-height: 100px;
	min-width: 150px;
	overflow: auto;
}

.definition-popover-filename {
	color: var(--text-faint);
	float: right;
}

.def-decoration {
	text-decoration: underline var(--color-yellow) dotted;
	-webkit-text-decoration: underline var(--color-yellow) dotted;
}

.def-link-decoration {
	text-decoration: underline var(--color-green) dotted;
	-webkit-text-decoration: underline var(--color-green) dotted;
	cursor: pointer;
}

.edit-modal-section-header {
	margin-top: 5px;
	margin-bottom: 5px;
	color: var(--text-muted);
}

.edit-modal-aliases {
	width: 100%;
	resize: none;
	font-size: var(--font-ui-medium);
	height: 2em;
}

.edit-modal-textarea {
	width: 100%;
	height: 20vh;
	resize: none;
	font-size: var(--font-ui-medium);
	margin-bottom: 10px;
}

.edit-modal-save-button {
	font-size: var(--font-ui-medium);
	float: right;
	background-color: var(--interactive-normal);
}

.popover-go-to-def-button {
	position: absolute;
	top: 1em;
	right: 1em;

	background-color: var(--interactive-normal);
	opacity: 0.2;
	transition-duration: 0.1s;
}
.popover-go-to-def-button:hover {
	opacity: 1;
}


================================================
FILE: tsconfig.json
================================================
{
	"compilerOptions": {
		"baseUrl": ".",
		"esModuleInterop": true,
		"inlineSourceMap": true,
		"inlineSources": true,
		"module": "ESNext",
		"target": "ES6",
		"allowJs": true,
		"noImplicitAny": true,
		"moduleResolution": "node",
		"importHelpers": true,
		"isolatedModules": true,
		"strictNullChecks": true,
		"lib": ["DOM", "ES5", "ES6", "ES7"]
	},
	"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
================================================
{
	"0.0.4": "1.5.12",
	"0.1.0": "1.5.12",
	"0.1.1": "1.5.12",
	"0.2.0": "1.5.12",
	"0.2.1": "1.5.12",
	"0.3.0": "1.5.12",
	"0.4.0": "1.5.12",
	"0.5.0": "1.5.12",
	"0.6.0": "1.5.12",
	"0.7.0": "1.5.12",
	"0.8.0": "1.5.12",
	"0.9.0": "1.5.12",
	"0.9.1": "1.5.12",
	"0.9.2": "1.5.12",
	"0.9.3": "1.5.12",
	"0.10.0": "1.5.12",
	"0.10.1": "1.5.12",
	"0.10.2": "1.5.12",
	"0.10.3": "1.5.12",
	"0.11.0": "1.5.12",
	"0.12.0": "1.5.12",
	"0.12.1": "1.5.12",
	"0.13.0": "1.5.12",
	"0.13.1": "1.5.12",
	"0.14.0": "1.5.12",
	"0.14.1": "1.5.12",
	"0.15.0": "1.5.12",
	"0.16.0": "1.5.12",
	"0.16.1": "1.5.12",
	"0.16.2": "1.5.12",
	"0.17.0": "1.5.12",
	"0.17.1": "1.5.12",
	"0.18.0": "1.5.12",
	"0.18.1": "1.5.12",
	"0.19.0": "1.5.12",
	"0.20.0": "1.5.12",
	"0.21.0": "1.5.12",
	"0.22.0": "1.5.12",
	"0.23.0": "1.5.12",
	"0.24.0": "1.5.12",
	"0.25.0": "1.5.12",
	"0.25.2": "1.5.12",
	"0.25.3": "1.5.12",
	"0.26.0": "1.5.12",
	"0.26.1": "1.5.12",
	"0.27.0": "1.5.12",
	"0.27.1": "1.5.12",
	"0.27.2": "1.5.12",
	"0.28.0": "1.5.12",
	"0.28.1": "1.5.12",
	"0.28.2": "1.5.12",
	"0.28.3": "1.5.12",
	"0.28.4": "1.5.12",
	"0.28.5": "1.5.12",
	"0.28.6": "1.5.12",
	"0.28.7": "1.5.12",
	"0.28.8": "1.5.12",
	"0.29.0": "1.5.12",
	"0.29.1": "1.5.12"
}
Download .txt
gitextract_c7dro_a6/

├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github/
│   └── workflows/
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .npmrc
├── .prettierignore
├── LICENSE
├── README.md
├── __mocks__/
│   ├── internals.ts
│   └── obsidian.ts
├── babel.config.js
├── docs/
│   └── grammar.md
├── esbuild.config.mjs
├── jest.config.js
├── manifest.json
├── package.json
├── src/
│   ├── core/
│   │   ├── atomic-def-parser.ts
│   │   ├── base-def-parser.ts
│   │   ├── consolidated-def-parser.ts
│   │   ├── def-file-manager.ts
│   │   ├── def-file-updater.ts
│   │   ├── file-parser.ts
│   │   ├── file-type.ts
│   │   ├── fm-builder.ts
│   │   └── model.ts
│   ├── editor/
│   │   ├── add-modal.ts
│   │   ├── common.ts
│   │   ├── decoration.ts
│   │   ├── def-file-registration.ts
│   │   ├── definition-popover.ts
│   │   ├── definition-search.ts
│   │   ├── edit-modal.ts
│   │   ├── frontmatter-suggest-modal.ts
│   │   ├── md-postprocessor.ts
│   │   ├── mobile/
│   │   │   └── definition-modal.ts
│   │   └── prefix-tree.ts
│   ├── globals.ts
│   ├── main.ts
│   ├── settings.ts
│   ├── tests/
│   │   ├── consolidated-def-parser.test.ts
│   │   ├── decorator.test.ts
│   │   ├── def-file-samples/
│   │   │   ├── case-sensitve-definitions-test.md
│   │   │   ├── consolidated-definitions-test.md
│   │   │   ├── consolidated-start-of-file-whitespace.md
│   │   │   ├── consolidated-trailing-delimiter.md
│   │   │   └── consolidated-trailing-whitespace.md
│   │   └── def-file-updater.test.ts
│   ├── types/
│   │   └── obsidian.d.ts
│   ├── ui/
│   │   └── file-explorer.ts
│   └── util/
│       ├── editor.ts
│       ├── log.ts
│       └── retry.ts
├── styles.css
├── tsconfig.json
├── version-bump.mjs
└── versions.json
Download .txt
SYMBOL INDEX (227 symbols across 30 files)

FILE: __mocks__/internals.ts
  class DefManager (line 1) | class DefManager {
    method loadUpdatedFiles (line 2) | loadUpdatedFiles() {}

FILE: __mocks__/obsidian.ts
  class App (line 1) | class App {
    method constructor (line 5) | constructor() {
  class TFile (line 11) | class TFile {
  class PluginSettingTab (line 18) | class PluginSettingTab {}
  class Vault (line 20) | class Vault {
    method modify (line 21) | modify(file: TFile, data: string) {}
    method read (line 22) | read(file: TFile): Promise<string> {
  class MetadataCache (line 27) | class MetadataCache {
    method getFileCache (line 28) | getFileCache(file: TFile) {
  class Notice (line 33) | class Notice {}

FILE: src/core/atomic-def-parser.ts
  class AtomicDefParser (line 6) | class AtomicDefParser extends BaseDefParser {
    method constructor (line 10) | constructor(app: App, file: TFile) {
    method parseFile (line 17) | async parseFile(fileContent?: string): Promise<Definition[]> {

FILE: src/core/base-def-parser.ts
  class BaseDefParser (line 5) | class BaseDefParser {
    method constructor (line 8) | constructor(parseSettings?: DefFileParseConfig) {
    method calculatePlurals (line 14) | calculatePlurals(aliases: string[]) {
    method getParseSettings (line 29) | getParseSettings(): DefFileParseConfig {

FILE: src/core/consolidated-def-parser.ts
  type DocAST (line 7) | interface DocAST {
  type DefblockAST (line 11) | interface DefblockAST {
  constant EOF (line 18) | const EOF = "";
  class ConsolidatedDefParser (line 20) | class ConsolidatedDefParser extends BaseDefParser {
    method constructor (line 29) | constructor(app: App, file: TFile, parseSettings?: DefFileParseConfig) {
    method parseFile (line 43) | async parseFile(fileContent?: string): Promise<Definition[]> {
    method directParseFile (line 62) | directParseFile(fileContent: string): Definition[] {
    method parseDoc (line 70) | private parseDoc(): DocAST {
    method parseDefBlock (line 95) | private parseDefBlock(): DefblockAST {
    method parseHeader (line 112) | private parseHeader(): string {
    method parseAliases (line 138) | private parseAliases(): string[] {
    method parseDef (line 175) | private parseDef(): string {
    method checkDelimiter (line 193) | private checkDelimiter(d: string) {
    method spitChar (line 199) | private spitChar(count?: number) {
    method consumeChar (line 208) | private consumeChar(): string {
    method headerToKey (line 219) | private headerToKey(key: string): string {
    method defBlockToDefinition (line 223) | private defBlockToDefinition(blk: DefblockAST): Definition {

FILE: src/core/def-file-manager.ts
  constant DEF_CTX_FM_KEY (line 14) | const DEF_CTX_FM_KEY = "def-context";
  class DefManager (line 16) | class DefManager {
    method constructor (line 34) | constructor(app: App) {
    method addDefFile (line 52) | addDefFile(file: TFile) {
    method getPrefixTree (line 57) | getPrefixTree() {
    method updateActiveFile (line 65) | updateActiveFile() {
    method updateDefSources (line 95) | updateDefSources(defSource: string[]) {
    method markDirty (line 106) | markDirty(file: TFile) {
    method flattenPathList (line 110) | private flattenPathList(paths: string[]): string[] {
    method flattenFolder (line 123) | private flattenFolder(path: string): string[] {
    method getChildrenFiles (line 135) | private getChildrenFiles(folder: TFolder): TFile[] {
    method isFolderPath (line 147) | private isFolderPath(path: string): boolean {
    method buildLocalPrefixTree (line 152) | private buildLocalPrefixTree(filePaths: string[]) {
    method buildLocalDefRepo (line 168) | private buildLocalDefRepo(filePaths: string[]) {
    method isDefFile (line 177) | isDefFile(file: TFile): boolean {
    method reset (line 184) | reset() {
    method loadDefinitions (line 193) | loadDefinitions() {
    method getDefRepo (line 198) | private getDefRepo() {
    method get (line 202) | get(key: string) {
    method set (line 206) | set(def: Definition) {
    method getDefFiles (line 210) | getDefFiles(): TFile[] {
    method getConsolidatedDefFiles (line 214) | getConsolidatedDefFiles(): TFile[] {
    method getDefFolders (line 218) | getDefFolders(): TFolder[] {
    method loadUpdatedFiles (line 222) | async loadUpdatedFiles() {
    method resetLocalConfigs (line 255) | private resetLocalConfigs() {
    method loadGlobals (line 261) | private async loadGlobals() {
    method buildPrefixTree (line 291) | private async buildPrefixTree() {
    method parseFolder (line 299) | private async parseFolder(folder: TFolder): Promise<Definition[]> {
    method parseFile (line 314) | private async parseFile(file: TFile): Promise<Definition[]> {
    method getDefFilesAndFolders (line 325) | getDefFilesAndFolders(): [TFolder[], TFile[]] {
    method walkFolder (line 336) | private walkFolder(folder: TFolder): [TFolder[], TFile[]] {
    method getGlobalDefFolder (line 353) | getGlobalDefFolder() {
  class DefinitionRepo (line 358) | class DefinitionRepo {
    method constructor (line 362) | constructor() {
    method getMapForFile (line 366) | getMapForFile(filePath: string) {
    method get (line 370) | get(key: string) {
    method getAllKeys (line 379) | getAllKeys(): string[] {
    method set (line 387) | set(def: Definition) {
    method clearForFile (line 410) | clearForFile(filePath: string) {
    method clear (line 417) | clear() {
  function initDefFileManager (line 422) | function initDefFileManager(app: App): DefManager {
  function getDefFileManager (line 427) | function getDefFileManager(): DefManager {

FILE: src/core/def-file-updater.ts
  class DefFileUpdater (line 10) | class DefFileUpdater {
    method constructor (line 13) | constructor(app: App) {
    method updateDefinition (line 17) | async updateDefinition(def: Definition) {
    method updateAtomicDefFile (line 33) | private async updateAtomicDefFile(def: Definition) {
    method updateConsolidatedDefFile (line 37) | private async updateConsolidatedDefFile(def: Definition) {
    method addDefinition (line 71) | async addDefinition(def: Partial<Definition>, folder?: string) {
    method addAtomicFileDefinition (line 87) | private async addAtomicFileDefinition(
    method addConsolidatedFileDefinition (line 118) | private async addConsolidatedFileDefinition(def: Partial<Definition>) {
    method addSeparator (line 145) | private addSeparator(lines: string[]) {
    method constructLinesFromDef (line 151) | private constructLinesFromDef(def: Partial<Definition>): string[] {
    method generateConsDefFile (line 166) | private generateConsDefFile(defs: Definition[]): string {

FILE: src/core/file-parser.ts
  constant DEF_TYPE_FM (line 9) | const DEF_TYPE_FM = "def-type";
  class FileParser (line 11) | class FileParser {
    method constructor (line 16) | constructor(app: App, file: TFile) {
    method parseFile (line 23) | async parseFile(fileContent?: string): Promise<Definition[]> {
    method getDefFileType (line 39) | private async getDefFileType(): Promise<DefFileType> {

FILE: src/core/file-type.ts
  type DefFileType (line 1) | enum DefFileType {

FILE: src/core/fm-builder.ts
  class FrontmatterBuilder (line 1) | class FrontmatterBuilder {
    method constructor (line 4) | constructor() {
    method add (line 8) | add(k: string, v: string) {
    method finish (line 12) | finish(): string {

FILE: src/core/model.ts
  type Definition (line 4) | interface Definition {
  type FilePosition (line 16) | interface FilePosition {

FILE: src/editor/add-modal.ts
  class AddDefinitionModal (line 13) | class AddDefinitionModal {
    method constructor (line 28) | constructor(app: App) {
    method open (line 33) | async open(text?: string) {
    method try_submit (line 208) | try_submit(

FILE: src/editor/common.ts
  constant DEF_DECORATION_CLS (line 7) | const DEF_DECORATION_CLS = "def-decoration";
  function getDecorationAttrs (line 10) | function getDecorationAttrs(phrase: string): { [key: string]: string } {

FILE: src/editor/decoration.ts
  type PhraseInfo (line 17) | interface PhraseInfo {
  function getMarkedPhrases (line 25) | function getMarkedPhrases(): PhraseInfo[] {
  class DefinitionMarker (line 30) | class DefinitionMarker implements PluginValue {
    method constructor (line 34) | constructor(view: EditorView) {
    method update (line 39) | update(update: ViewUpdate) {
    method forceUpdate (line 53) | public forceUpdate() {
    method destroy (line 61) | destroy() {}
    method buildDecorations (line 63) | buildDecorations(view: EditorView): DecorationSet {
  function scanText (line 90) | function scanText(
  function removeSubsetsAndIntersects (line 113) | function removeSubsetsAndIntersects(phraseInfos: PhraseInfo[]): PhraseIn...

FILE: src/editor/def-file-registration.ts
  function registerDefFile (line 7) | function registerDefFile(app: App, file: TFile, fileType: DefFileType) {

FILE: src/editor/definition-popover.ts
  constant DEF_POPOVER_ID (line 14) | const DEF_POPOVER_ID = "definition-popover";
  type Coordinates (line 18) | interface Coordinates {
  class DefinitionPopover (line 25) | class DefinitionPopover extends Component {
    method constructor (line 34) | constructor(plugin: Plugin) {
    method openAtCursor (line 42) | openAtCursor(def: Definition) {
    method openAtCoords (line 55) | openAtCoords(def: Definition, coords: Coordinates) {
    method cleanUp (line 66) | cleanUp() {
    method getCmEditor (line 85) | private getCmEditor(app: App) {
    method shouldOpenToLeft (line 96) | private shouldOpenToLeft(
    method shouldOpenUpwards (line 103) | private shouldOpenUpwards(
    method createElement (line 111) | private createElement(
    method postprocessMarkdown (line 165) | private postprocessMarkdown(el: HTMLDivElement, def: Definition) {
    method mountAtCursor (line 186) | private mountAtCursor(def: Definition) {
    method offsetCoordsToContainer (line 201) | private offsetCoordsToContainer(
    method mountAtCoordinates (line 214) | private mountAtCoordinates(def: Definition, coords: Coordinates) {
    method positionAndSizePopover (line 226) | private positionAndSizePopover(mdView: MarkdownView, coords: Coordinat...
    method unmount (line 276) | private unmount() {
    method getCursorCoords (line 289) | private getCursorCoords(): Coordinates {
    method registerClosePopoverListeners (line 298) | private registerClosePopoverListeners() {
    method unregisterClosePopoverListeners (line 328) | private unregisterClosePopoverListeners() {
    method getCmScroller (line 347) | private getCmScroller() {
    method getPopoverElement (line 354) | getPopoverElement() {
    method getActiveView (line 358) | private getActiveView() {
  function initDefinitionPopover (line 364) | function initDefinitionPopover(plugin: Plugin) {
  function getDefinitionPopover (line 371) | function getDefinitionPopover() {

FILE: src/editor/definition-search.ts
  type PhraseInfo (line 6) | interface PhraseInfo {
  class LineScanner (line 12) | class LineScanner {
    method constructor (line 19) | constructor(pTree?: PTreeNode) {
    method scanLine (line 23) | scanLine(line: string, offset?: number): PhraseInfo[] {
    method isValidEnd (line 59) | private isValidEnd(line: string, ptr: number): boolean {
    method isValidStart (line 79) | private isValidStart(line: string, ptr: number): boolean {
    method isNonSpacedLanguage (line 97) | private isNonSpacedLanguage(c: string): boolean {

FILE: src/editor/edit-modal.ts
  class EditDefinitionModal (line 5) | class EditDefinitionModal {
    method constructor (line 12) | constructor(app: App) {
    method open (line 17) | open(def: Definition) {
    method submit (line 58) | submit(

FILE: src/editor/frontmatter-suggest-modal.ts
  class FMSuggestModal (line 12) | class FMSuggestModal extends FuzzySuggestModal<TAbstractFile> {
    method constructor (line 15) | constructor(app: App, currFile: TFile) {
    method getItems (line 20) | getItems(): TAbstractFile[] {
    method getItemText (line 25) | getItemText(item: TAbstractFile): string {
    method onChooseItem (line 29) | onChooseItem(item: TAbstractFile, evt: MouseEvent | KeyboardEvent) {
    method getPath (line 51) | private getPath(file: TAbstractFile): string {

FILE: src/editor/md-postprocessor.ts
  constant DEF_LINK_DECOR_CLS (line 8) | const DEF_LINK_DECOR_CLS = "def-link-decoration";
  type Marks (line 10) | interface Marks {
  function getNormalDecorationSpan (line 107) | function getNormalDecorationSpan(
  function getLinkDecorationSpan (line 121) | function getLinkDecorationSpan(

FILE: src/editor/mobile/definition-modal.ts
  class DefinitionModal (line 12) | class DefinitionModal extends Component {
    method constructor (line 16) | constructor(app: App) {
    method open (line 22) | open(definition: Definition) {
  function initDefinitionModal (line 46) | function initDefinitionModal(app: App) {
  function getDefinitionModal (line 51) | function getDefinitionModal() {

FILE: src/editor/prefix-tree.ts
  class PTreeNode (line 2) | class PTreeNode {
    method constructor (line 6) | constructor() {
    method add (line 11) | add(word: string, ptr?: number) {
  class PTreeTraverser (line 31) | class PTreeTraverser {
    method constructor (line 35) | constructor(root: PTreeNode) {
    method gotoNext (line 40) | gotoNext(c: string) {
    method isWordEnd (line 51) | isWordEnd() {
    method getWord (line 58) | getWord() {

FILE: src/globals.ts
  type Window (line 11) | interface Window {
  type GlobalVars (line 16) | interface GlobalVars {
  function injectGlobals (line 27) | function injectGlobals(

FILE: src/main.ts
  class NoteDefinition (line 35) | class NoteDefinition extends Plugin {
    method onload (line 40) | async onload() {
    method saveSettings (line 78) | async saveSettings() {
    method registerCommands (line 84) | registerCommands() {
    method registerEvents (line 176) | registerEvents() {
    method registerMenuForMarkedWords (line 268) | registerMenuForMarkedWords(menu: Menu, def: Definition) {
    method refreshDefinitions (line 287) | refreshDefinitions() {
    method reloadUpdatedDefinitions (line 291) | reloadUpdatedDefinitions() {
    method updateEditorExts (line 295) | updateEditorExts() {
    method setActiveEditorExtensions (line 305) | private setActiveEditorExtensions(...ext: Extension[]) {
    method onunload (line 311) | onunload() {

FILE: src/settings.ts
  type PopoverEventSettings (line 12) | enum PopoverEventSettings {
  type PopoverDismissType (line 17) | enum PopoverDismissType {
  type DividerSettings (line 22) | interface DividerSettings {
  type DefFileParseConfig (line 27) | interface DefFileParseConfig {
  type DefinitionPopoverConfig (line 34) | interface DefinitionPopoverConfig {
  type Settings (line 45) | interface Settings {
  constant VALID_DEFINITION_FILE_TYPES (line 55) | const VALID_DEFINITION_FILE_TYPES = [".md"];
  constant DEFAULT_DEF_FOLDER (line 57) | const DEFAULT_DEF_FOLDER = "definitions";
  constant DEFAULT_SETTINGS (line 59) | const DEFAULT_SETTINGS: Partial<Settings> = {
  class SettingsTab (line 84) | class SettingsTab extends PluginSettingTab {
    method constructor (line 89) | constructor(app: App, plugin: Plugin, saveCallback: () => Promise<void...
    method display (line 96) | display(): void {
  function getSettings (line 457) | function getSettings(): Settings {

FILE: src/types/obsidian.d.ts
  type FileExplorerView (line 3) | interface FileExplorerView extends View {
  type FileItem (line 7) | interface FileItem {

FILE: src/ui/file-explorer.ts
  constant MAX_RETRY (line 12) | const MAX_RETRY = 3;
  constant RETRY_INTERVAL (line 13) | const RETRY_INTERVAL = 1000;
  constant DIV_ID (line 14) | const DIV_ID = "def-tag-id";
  class FileExplorerDecoration (line 16) | class FileExplorerDecoration {
    method constructor (line 20) | constructor(app: App) {
    method run (line 25) | async run() {
    method exec (line 42) | private exec() {
    method tagFile (line 83) | private tagFile(
  function initFileExplorerDecoration (line 110) | function initFileExplorerDecoration(app: App): FileExplorerDecoration {
  function getFileExplorerDecoration (line 115) | function getFileExplorerDecoration(app: App): FileExplorerDecoration {

FILE: src/util/editor.ts
  function getMarkedWordUnderCursor (line 6) | function getMarkedWordUnderCursor(editor: Editor) {
  function normaliseWord (line 11) | function normaliseWord(word: string) {
  function getWordByOffset (line 18) | function getWordByOffset(offset: number): string {

FILE: src/util/log.ts
  type LogLevel (line 3) | enum LogLevel {
  function logWithLevel (line 20) | function logWithLevel(msg: string, logLevel: LogLevel) {
  function logDebug (line 28) | function logDebug(msg: string) {
  function logInfo (line 32) | function logInfo(msg: string) {
  function logWarn (line 36) | function logWarn(msg: string) {
  function logError (line 40) | function logError(msg: string) {

FILE: src/util/retry.ts
  constant RETRY_INTERVAL (line 1) | const RETRY_INTERVAL = 1000;
  function useRetry (line 3) | function useRetry(retryCount?: number) {
Condensed preview — 59 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (142K chars).
[
  {
    "path": ".editorconfig",
    "chars": 166,
    "preview": "# top-most EditorConfig file\r\nroot = true\r\n\r\n[*]\r\ncharset = utf-8\r\nend_of_line = lf\r\ninsert_final_newline = true\r\nindent"
  },
  {
    "path": ".eslintignore",
    "chars": 23,
    "preview": "node_modules/\n\nmain.js\n"
  },
  {
    "path": ".eslintrc",
    "chars": 627,
    "preview": "{\n    \"root\": true,\n    \"parser\": \"@typescript-eslint/parser\",\n    \"env\": { \"node\": true },\n    \"plugins\": [\n      \"@typ"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 636,
    "preview": "name: Release Obsidian plugin\n\non:\n  push:\n    tags:\n      - \"*\"\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 339,
    "preview": "name: Test\n\non:\n  pull_request:\n    branches:\n      - master\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n    "
  },
  {
    "path": ".gitignore",
    "chars": 316,
    "preview": "# vscode\r\n.vscode \r\n\r\n# Intellij\r\n*.iml\r\n.idea\r\n\r\n# npm\r\nnode_modules\r\n\r\n# Don't include the compiled main.js file in th"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 25,
    "preview": "npm test\nnpx lint-staged\n"
  },
  {
    "path": ".npmrc",
    "chars": 21,
    "preview": "tag-version-prefix=\"\""
  },
  {
    "path": ".prettierignore",
    "chars": 110,
    "preview": "# Ignore docs\n**/*.md\n\n# JS files are generated, no need to format\n*.js\n*.mjs\n\n# Ignore github files\n.github/\n"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "MIT License\n\nCopyright (c) 2024 dominiclet\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 7856,
    "preview": "# Obsidian Note Definitions\n\nA personal dictionary that can be easily looked-up from within your notes.\n\n![dropdown](./i"
  },
  {
    "path": "__mocks__/internals.ts",
    "chars": 51,
    "preview": "export class DefManager {\n\tloadUpdatedFiles() {}\n}\n"
  },
  {
    "path": "__mocks__/obsidian.ts",
    "chars": 523,
    "preview": "export class App {\n\tvault: Vault;\n\tmetadataCache: MetadataCache;\n\n\tconstructor() {\n\t\tthis.vault = new Vault();\n\t\tthis.me"
  },
  {
    "path": "babel.config.js",
    "chars": 78,
    "preview": "module.exports = {presets: ['@babel/preset-env', \"@babel/preset-typescript\"]}\n"
  },
  {
    "path": "docs/grammar.md",
    "chars": 941,
    "preview": "# Definition File Grammar\n\nThis file documents the formal grammar defined for the definition files. It should give you s"
  },
  {
    "path": "esbuild.config.mjs",
    "chars": 958,
    "preview": "import esbuild from \"esbuild\";\nimport process from \"process\";\nimport builtins from \"builtin-modules\";\n\nconst banner =\n`/"
  },
  {
    "path": "jest.config.js",
    "chars": 258,
    "preview": "module.exports = {\n    preset: \"ts-jest\",\n    testEnvironment: \"node\",\n\ttransform: {\n\t\t'^.+\\\\.ts$': 'ts-jest',\n\t},\n\tmodu"
  },
  {
    "path": "manifest.json",
    "chars": 214,
    "preview": "{\n\t\"id\": \"note-definitions\",\n\t\"name\": \"Note Definitions\",\n\t\"version\": \"0.29.1\",\n\t\"minAppVersion\": \"1.5.12\",\n\t\"descriptio"
  },
  {
    "path": "package.json",
    "chars": 991,
    "preview": "{\n\t\"name\": \"obsidian-note-definitions\",\n\t\"version\": \"0.29.1\",\n\t\"description\": \"Personal dictionary for your notes\",\n\t\"ma"
  },
  {
    "path": "src/core/atomic-def-parser.ts",
    "chars": 1292,
    "preview": "import { BaseDefParser } from \"./base-def-parser\";\nimport { App, TFile } from \"obsidian\";\nimport { Definition } from \"./"
  },
  {
    "path": "src/core/base-def-parser.ts",
    "chars": 661,
    "preview": "import { DefFileParseConfig, getSettings } from \"src/settings\";\n\nvar pluralize = require(\"pluralize\");\n\nexport class Bas"
  },
  {
    "path": "src/core/consolidated-def-parser.ts",
    "chars": 5412,
    "preview": "import { App, TFile } from \"obsidian\";\nimport { BaseDefParser } from \"src/core/base-def-parser\";\nimport { DefFileParseCo"
  },
  {
    "path": "src/core/def-file-manager.ts",
    "chars": 10868,
    "preview": "import { App, TFile, TFolder } from \"obsidian\";\nimport { PTreeNode } from \"src/editor/prefix-tree\";\nimport { DEFAULT_DEF"
  },
  {
    "path": "src/core/def-file-updater.ts",
    "chars": 5193,
    "preview": "import { App, Notice } from \"obsidian\";\nimport { getSettings } from \"src/settings\";\nimport { logError, logWarn } from \"s"
  },
  {
    "path": "src/core/file-parser.ts",
    "chars": 1990,
    "preview": "import { App, CachedMetadata, TFile } from \"obsidian\";\nimport { getSettings } from \"src/settings\";\nimport { useRetry } f"
  },
  {
    "path": "src/core/file-type.ts",
    "chars": 80,
    "preview": "export enum DefFileType {\n\tConsolidated = \"consolidated\",\n\tAtomic = \"atomic\",\n}\n"
  },
  {
    "path": "src/core/fm-builder.ts",
    "chars": 311,
    "preview": "export class FrontmatterBuilder {\n\tfm: Map<string, string>;\n\n\tconstructor() {\n\t\tthis.fm = new Map<string, string>();\n\t}\n"
  },
  {
    "path": "src/core/model.ts",
    "chars": 356,
    "preview": "import { TFile } from \"obsidian\";\nimport { DefFileType } from \"./file-type\";\n\nexport interface Definition {\n\tkey: string"
  },
  {
    "path": "src/editor/add-modal.ts",
    "chars": 7611,
    "preview": "import {\n\tApp,\n\tDropdownComponent,\n\tModal,\n\tNotice,\n\tSetting,\n\tTFile,\n} from \"obsidian\";\nimport { getDefFileManager, DEF"
  },
  {
    "path": "src/editor/common.ts",
    "chars": 812,
    "preview": "import { Platform } from \"obsidian\";\nimport { getSettings, PopoverEventSettings } from \"src/settings\";\n\nconst triggerFun"
  },
  {
    "path": "src/editor/decoration.ts",
    "chars": 3297,
    "preview": "import { RangeSetBuilder } from \"@codemirror/state\";\nimport {\n\tDecoration,\n\tDecorationSet,\n\tEditorView,\n\tPluginSpec,\n\tPl"
  },
  {
    "path": "src/editor/def-file-registration.ts",
    "chars": 542,
    "preview": "import { App, TFile } from \"obsidian\";\nimport { getDefFileManager } from \"src/core/def-file-manager\";\nimport { DEF_TYPE_"
  },
  {
    "path": "src/editor/definition-popover.ts",
    "chars": 9804,
    "preview": "import {\n\tApp,\n\tButtonComponent,\n\tComponent,\n\tMarkdownRenderer,\n\tMarkdownView,\n\tnormalizePath,\n\tPlugin,\n} from \"obsidian"
  },
  {
    "path": "src/editor/definition-search.ts",
    "chars": 2713,
    "preview": "import { getDefFileManager } from \"src/core/def-file-manager\";\nimport { PTreeNode, PTreeTraverser } from \"./prefix-tree\""
  },
  {
    "path": "src/editor/edit-modal.ts",
    "chars": 1834,
    "preview": "import { App, Modal } from \"obsidian\";\nimport { DefFileUpdater } from \"src/core/def-file-updater\";\nimport { Definition }"
  },
  {
    "path": "src/editor/frontmatter-suggest-modal.ts",
    "chars": 1356,
    "preview": "import {\n\tApp,\n\tFuzzySuggestModal,\n\tNotice,\n\tTAbstractFile,\n\tTFile,\n\tTFolder,\n} from \"obsidian\";\nimport { DEF_CTX_FM_KEY"
  },
  {
    "path": "src/editor/md-postprocessor.ts",
    "chars": 3879,
    "preview": "import { MarkdownPostProcessor } from \"obsidian\";\nimport { getDefFileManager } from \"src/core/def-file-manager\";\nimport "
  },
  {
    "path": "src/editor/mobile/definition-modal.ts",
    "chars": 992,
    "preview": "import {\n\tApp,\n\tComponent,\n\tMarkdownRenderer,\n\tnormalizePath,\n\tModal,\n} from \"obsidian\";\nimport { Definition } from \"src"
  },
  {
    "path": "src/editor/prefix-tree.ts",
    "chars": 1237,
    "preview": "// Prefix tree node\nexport class PTreeNode {\n\tchildren: Map<string, PTreeNode>;\n\twordEnd: boolean;\n\n\tconstructor() {\n\t\tt"
  },
  {
    "path": "src/globals.ts",
    "chars": 1845,
    "preview": "import { App, Platform } from \"obsidian\";\nimport { DefinitionRepo, getDefFileManager } from \"./core/def-file-manager\";\ni"
  },
  {
    "path": "src/main.ts",
    "chars": 8240,
    "preview": "import {\n\tMenu,\n\tNotice,\n\tPlugin,\n\tTFolder,\n\tWorkspaceWindow,\n\tTFile,\n\tMarkdownView,\n} from \"obsidian\";\nimport { injectG"
  },
  {
    "path": "src/settings.ts",
    "chars": 12948,
    "preview": "import {\n\tApp,\n\tModal,\n\tNotice,\n\tPlugin,\n\tPluginSettingTab,\n\tSetting,\n\tsetTooltip,\n} from \"obsidian\";\nimport { DefFileTy"
  },
  {
    "path": "src/tests/consolidated-def-parser.test.ts",
    "chars": 8122,
    "preview": "import { App, TFile } from \"obsidian\";\nimport { ConsolidatedDefParser } from \"src/core/consolidated-def-parser\";\nimport "
  },
  {
    "path": "src/tests/decorator.test.ts",
    "chars": 2821,
    "preview": "import { scanText } from \"src/editor/decoration\";\nimport { PhraseInfo } from \"src/editor/definition-search\";\nimport { PT"
  },
  {
    "path": "src/tests/def-file-samples/case-sensitve-definitions-test.md",
    "chars": 452,
    "preview": "# first\n\nThis is the first definition to test basic functionality.\n\n---\n\n# First\n\nThis is a different definition than th"
  },
  {
    "path": "src/tests/def-file-samples/consolidated-definitions-test.md",
    "chars": 295,
    "preview": "# First\n\nThis is the first definition to test basic functionality.\n\n---\n\n# Multiple-word definition\n\nThis ensures that m"
  },
  {
    "path": "src/tests/def-file-samples/consolidated-start-of-file-whitespace.md",
    "chars": 305,
    "preview": "\n\n\n\n\n\n \n # First\n\nThis is the first definition to test basic functionality.\n\n---\n\n# Multiple-word definition\n\nThis ensur"
  },
  {
    "path": "src/tests/def-file-samples/consolidated-trailing-delimiter.md",
    "chars": 306,
    "preview": "# First\n\nThis is the first definition to test basic functionality.\n\n---\n\n# Multiple-word definition\n\nThis ensures that m"
  },
  {
    "path": "src/tests/def-file-samples/consolidated-trailing-whitespace.md",
    "chars": 300,
    "preview": "# First\n\nThis is the first definition to test basic functionality.\n\n---\n\n# Multiple-word definition\n\nThis ensures that m"
  },
  {
    "path": "src/tests/def-file-updater.test.ts",
    "chars": 3671,
    "preview": "import { App, TFile } from \"obsidian\";\nimport { DefFileUpdater } from \"src/core/def-file-updater\";\nimport { DefFileType "
  },
  {
    "path": "src/types/obsidian.d.ts",
    "chars": 188,
    "preview": "import { View } from \"obsidian\";\n\ninterface FileExplorerView extends View {\n\tfileItems: { [key: string]: FileItem };\n}\n\n"
  },
  {
    "path": "src/ui/file-explorer.ts",
    "chars": 2855,
    "preview": "import { App } from \"obsidian\";\nimport {\n\tDEFAULT_DEF_FOLDER,\n\tgetSettings,\n\tVALID_DEFINITION_FILE_TYPES,\n} from \"src/se"
  },
  {
    "path": "src/util/editor.ts",
    "chars": 1048,
    "preview": "import { Editor } from \"obsidian\";\nimport { getMarkedPhrases } from \"src/editor/decoration\";\nimport { getSettings } from"
  },
  {
    "path": "src/util/log.ts",
    "chars": 784,
    "preview": "// Rudimentary logger implementation\n\nexport enum LogLevel {\n\tSilent,\n\tError,\n\tWarn,\n\tInfo,\n\tDebug,\n}\n\nconst levelMap = "
  },
  {
    "path": "src/util/retry.ts",
    "chars": 542,
    "preview": "const RETRY_INTERVAL = 1000;\n\nexport function useRetry(retryCount?: number) {\n\tlet shouldRetry = false;\n\tlet maxRetries "
  },
  {
    "path": "styles.css",
    "chars": 1362,
    "preview": ".definition-popover {\n\tbackground-color: var(--background-secondary);\n\tborder: 1px solid var(--background-modifier-borde"
  },
  {
    "path": "tsconfig.json",
    "chars": 384,
    "preview": "{\n\t\"compilerOptions\": {\n\t\t\"baseUrl\": \".\",\n\t\t\"esModuleInterop\": true,\n\t\t\"inlineSourceMap\": true,\n\t\t\"inlineSources\": true,"
  },
  {
    "path": "version-bump.mjs",
    "chars": 648,
    "preview": "import { readFileSync, writeFileSync } from \"fs\";\n\nconst targetVersion = process.env.npm_package_version;\n\n// read minAp"
  },
  {
    "path": "versions.json",
    "chars": 1227,
    "preview": "{\n\t\"0.0.4\": \"1.5.12\",\n\t\"0.1.0\": \"1.5.12\",\n\t\"0.1.1\": \"1.5.12\",\n\t\"0.2.0\": \"1.5.12\",\n\t\"0.2.1\": \"1.5.12\",\n\t\"0.3.0\": \"1.5.12\""
  }
]

About this extraction

This page contains the full source code of the dominiclet/obsidian-note-definitions GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 59 files (120.9 KB), approximately 33.7k tokens, and a symbol index with 227 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!