mirror of
https://github.com/kdnk/obsidian-automatic-linker.git
synced 2026-07-22 05:37:46 +00:00
refactor(settings): centralize settings catalog
Why: - Adding or changing a setting required parallel edits across defaults, projections, refresh behavior, and UI rendering. - Task 3 needs one canonical catalog while preserving the existing Task 2 formatting behavior and current settings side effects. What: - Add a settings catalog module with defaults, UI metadata, index-refresh flags, and projection helpers. - Re-export settings info compatibility imports and reuse the catalog projection from formatting-run. - Render the settings tab from catalog entries while preserving labels, order, validation, and URL-title refresh behavior.
This commit is contained in:
parent
c6babf82ad
commit
36c6f45128
5 changed files with 566 additions and 570 deletions
|
|
@ -1,7 +1,6 @@
|
|||
import { isUrlTitleReplacementOff } from "./frontmatter-utils"
|
||||
import {
|
||||
LinkGenerator,
|
||||
ReplaceLinksSettings,
|
||||
replaceLinks,
|
||||
} from "./replace-links/replace-links"
|
||||
import { replaceUrlWithTitle } from "./replace-url-with-title"
|
||||
|
|
@ -9,7 +8,10 @@ import { formatGitHubURL } from "./replace-urls/github"
|
|||
import { formatJiraURL } from "./replace-urls/jira"
|
||||
import { formatLinearURL } from "./replace-urls/linear"
|
||||
import { replaceURLs } from "./replace-urls/replace-urls"
|
||||
import { AutomaticLinkerSettings } from "./settings/settings-info"
|
||||
import {
|
||||
AutomaticLinkerSettings,
|
||||
projectReplaceLinksSettings,
|
||||
} from "./settings/settings-catalog"
|
||||
import { CandidateData, TrieNode } from "./trie"
|
||||
|
||||
export interface CandidateIndex {
|
||||
|
|
@ -29,20 +31,7 @@ export interface FormattingRunOptions {
|
|||
linkGenerator?: LinkGenerator
|
||||
}
|
||||
|
||||
export const toReplaceLinksSettings = (
|
||||
settings: AutomaticLinkerSettings,
|
||||
baseDir?: string,
|
||||
): ReplaceLinksSettings => ({
|
||||
proximityBasedLinking: settings.proximityBasedLinking,
|
||||
baseDir,
|
||||
ignoreDateFormats: settings.ignoreDateFormats,
|
||||
ignoreCase: settings.ignoreCase,
|
||||
matchSentenceCase: settings.matchSentenceCase,
|
||||
preventSelfLinking: settings.preventSelfLinking,
|
||||
removeAliasInDirs: settings.removeAliasInDirs,
|
||||
ignoreHeadings: settings.ignoreHeadings,
|
||||
ignoreMarkdownTables: settings.ignoreMarkdownTables,
|
||||
})
|
||||
export { projectReplaceLinksSettings as toReplaceLinksSettings } from "./settings/settings-catalog"
|
||||
|
||||
const formatMarkdownURLs = (
|
||||
text: string,
|
||||
|
|
@ -86,7 +75,7 @@ export const formatMarkdownBody = ({
|
|||
trie: candidateIndex.trie,
|
||||
candidateMap: candidateIndex.candidateMap,
|
||||
},
|
||||
settings: toReplaceLinksSettings(settings, baseDir),
|
||||
settings: projectReplaceLinksSettings(settings, baseDir),
|
||||
linkGenerator,
|
||||
})
|
||||
}
|
||||
|
|
@ -113,7 +102,7 @@ export const formatMarkdownSelection = ({
|
|||
trie: candidateIndex.trie,
|
||||
candidateMap: candidateIndex.candidateMap,
|
||||
},
|
||||
settings: toReplaceLinksSettings(settings, baseDir),
|
||||
settings: projectReplaceLinksSettings(settings, baseDir),
|
||||
linkGenerator,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
73
src/settings/__tests__/settings-catalog.test.ts
Normal file
73
src/settings/__tests__/settings-catalog.test.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { describe, expect, it } from "vitest"
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
SETTINGS_CATALOG,
|
||||
projectReplaceLinksSettings,
|
||||
projectUrlFormattingSettings,
|
||||
settingRefreshesIndex,
|
||||
} from "../settings-catalog"
|
||||
|
||||
describe("SETTINGS_CATALOG", () => {
|
||||
it("covers every default setting exactly once", () => {
|
||||
const defaultKeys = Object.keys(DEFAULT_SETTINGS).sort()
|
||||
const catalogKeys = SETTINGS_CATALOG.map(entry => entry.key).sort()
|
||||
|
||||
expect(catalogKeys).toEqual(defaultKeys)
|
||||
expect(new Set(catalogKeys).size).toBe(catalogKeys.length)
|
||||
})
|
||||
|
||||
it("marks current index-refresh settings", () => {
|
||||
expect(settingRefreshesIndex("respectNewFileFolderPath")).toBe(true)
|
||||
expect(settingRefreshesIndex("includeAliases")).toBe(true)
|
||||
expect(settingRefreshesIndex("proximityBasedLinking")).toBe(true)
|
||||
expect(settingRefreshesIndex("ignoreDateFormats")).toBe(true)
|
||||
expect(settingRefreshesIndex("ignoreCase")).toBe(true)
|
||||
expect(settingRefreshesIndex("preventSelfLinking")).toBe(true)
|
||||
expect(settingRefreshesIndex("excludeDirsFromAutoLinking")).toBe(true)
|
||||
expect(settingRefreshesIndex("removeAliasInDirs")).toBe(true)
|
||||
expect(settingRefreshesIndex("debug")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("settings projections", () => {
|
||||
it("projects link replacement settings", () => {
|
||||
expect(projectReplaceLinksSettings({
|
||||
...DEFAULT_SETTINGS,
|
||||
proximityBasedLinking: false,
|
||||
ignoreDateFormats: false,
|
||||
ignoreCase: false,
|
||||
matchSentenceCase: false,
|
||||
preventSelfLinking: true,
|
||||
removeAliasInDirs: ["archive"],
|
||||
ignoreHeadings: true,
|
||||
ignoreMarkdownTables: true,
|
||||
}, "pages")).toEqual({
|
||||
proximityBasedLinking: false,
|
||||
baseDir: "pages",
|
||||
ignoreDateFormats: false,
|
||||
ignoreCase: false,
|
||||
matchSentenceCase: false,
|
||||
preventSelfLinking: true,
|
||||
removeAliasInDirs: ["archive"],
|
||||
ignoreHeadings: true,
|
||||
ignoreMarkdownTables: true,
|
||||
})
|
||||
})
|
||||
|
||||
it("projects URL formatting settings", () => {
|
||||
expect(projectUrlFormattingSettings({
|
||||
...DEFAULT_SETTINGS,
|
||||
formatGitHubURLs: false,
|
||||
githubEnterpriseURLs: ["github.enterprise.com"],
|
||||
formatJiraURLs: false,
|
||||
jiraURLs: ["jira.example.com"],
|
||||
formatLinearURLs: true,
|
||||
})).toEqual({
|
||||
formatGitHubURLs: false,
|
||||
githubEnterpriseURLs: ["github.enterprise.com"],
|
||||
formatJiraURLs: false,
|
||||
jiraURLs: ["jira.example.com"],
|
||||
formatLinearURLs: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
380
src/settings/settings-catalog.ts
Normal file
380
src/settings/settings-catalog.ts
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
import { ReplaceLinksSettings } from "../replace-links/replace-links"
|
||||
|
||||
export type AutomaticLinkerSettings = {
|
||||
formatOnSave: boolean
|
||||
showNotice: boolean
|
||||
respectNewFileFolderPath: boolean
|
||||
includeAliases: boolean
|
||||
proximityBasedLinking: boolean
|
||||
ignoreDateFormats: boolean
|
||||
ignoreHeadings: boolean
|
||||
formatGitHubURLs: boolean
|
||||
githubEnterpriseURLs: string[]
|
||||
formatJiraURLs: boolean
|
||||
jiraURLs: string[]
|
||||
formatLinearURLs: boolean
|
||||
debug: boolean
|
||||
ignoreCase: boolean
|
||||
matchSentenceCase: boolean
|
||||
replaceUrlWithTitle: boolean
|
||||
replaceUrlWithTitleIgnoreDomains: string[]
|
||||
excludeDirsFromAutoLinking: string[]
|
||||
preventSelfLinking: boolean
|
||||
removeAliasInDirs: string[]
|
||||
ignoreMarkdownTables: boolean
|
||||
runLinterAfterFormatting: boolean
|
||||
runPrettierAfterFormatting: boolean
|
||||
formatDelayMs: number
|
||||
aiEnabled: boolean
|
||||
aiEndpoint: string
|
||||
aiModel: string
|
||||
aiMaxContext: number
|
||||
}
|
||||
|
||||
export type SettingControl = "toggle" | "text" | "textarea"
|
||||
|
||||
export interface SettingCatalogEntry<K extends keyof AutomaticLinkerSettings = keyof AutomaticLinkerSettings> {
|
||||
key: K
|
||||
group: string
|
||||
name: string
|
||||
description: string
|
||||
control: SettingControl
|
||||
placeholder?: string
|
||||
multiline?: boolean
|
||||
refreshesIndex: boolean
|
||||
runtimeOnly?: boolean
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AutomaticLinkerSettings = {
|
||||
formatOnSave: false,
|
||||
showNotice: false,
|
||||
respectNewFileFolderPath: true,
|
||||
includeAliases: true,
|
||||
proximityBasedLinking: true,
|
||||
ignoreDateFormats: true,
|
||||
ignoreHeadings: false,
|
||||
formatGitHubURLs: true,
|
||||
githubEnterpriseURLs: [],
|
||||
formatJiraURLs: true,
|
||||
jiraURLs: [],
|
||||
formatLinearURLs: false,
|
||||
debug: false,
|
||||
ignoreCase: true,
|
||||
matchSentenceCase: true,
|
||||
replaceUrlWithTitle: true,
|
||||
replaceUrlWithTitleIgnoreDomains: [],
|
||||
excludeDirsFromAutoLinking: [],
|
||||
preventSelfLinking: false,
|
||||
removeAliasInDirs: [],
|
||||
ignoreMarkdownTables: false,
|
||||
runLinterAfterFormatting: false,
|
||||
runPrettierAfterFormatting: false,
|
||||
formatDelayMs: 1,
|
||||
aiEnabled: false,
|
||||
aiEndpoint: "http://localhost:1234/v1",
|
||||
aiModel: "gemma-4-7b",
|
||||
aiMaxContext: 500,
|
||||
}
|
||||
|
||||
export const SETTINGS_CATALOG = [
|
||||
{
|
||||
key: "formatOnSave",
|
||||
group: "Formatting",
|
||||
name: "Format on save",
|
||||
description:
|
||||
"When enabled, the file will be automatically formatted (links replaced) when saving.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "respectNewFileFolderPath",
|
||||
group: "Formatting",
|
||||
name: "Respect 'Folder to create new notes in' setting",
|
||||
description:
|
||||
"When enabled, the plugin will use Obsidian's 'Folder to create new notes in' setting as the base directory for omitting folder prefixes in links.",
|
||||
control: "toggle",
|
||||
refreshesIndex: true,
|
||||
},
|
||||
{
|
||||
key: "includeAliases",
|
||||
group: "Formatting",
|
||||
name: "Include aliases",
|
||||
description:
|
||||
"When enabled, aliases will be included when processing links. Note: A restart is required for changes to take effect.",
|
||||
control: "toggle",
|
||||
refreshesIndex: true,
|
||||
},
|
||||
{
|
||||
key: "proximityBasedLinking",
|
||||
group: "Formatting",
|
||||
name: "Proximity-based linking",
|
||||
description:
|
||||
"When enabled, the plugin will automatically resolve namespaces for shorthand links. If multiple candidates share the same shorthand, the candidate with the most common path segments relative to the current file will be selected.",
|
||||
control: "toggle",
|
||||
refreshesIndex: true,
|
||||
},
|
||||
{
|
||||
key: "ignoreDateFormats",
|
||||
group: "Formatting",
|
||||
name: "Ignore date formats",
|
||||
description:
|
||||
"When enabled, links that match date formats (e.g. 2025-02-10) will be ignored. This helps maintain compatibility with Obsidian Tasks.",
|
||||
control: "toggle",
|
||||
refreshesIndex: true,
|
||||
},
|
||||
{
|
||||
key: "ignoreHeadings",
|
||||
group: "Formatting",
|
||||
name: "Ignore headings",
|
||||
description:
|
||||
"When enabled, headings (lines starting with #) will not have links added to them.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "ignoreMarkdownTables",
|
||||
group: "Formatting",
|
||||
name: "Ignore Markdown tables",
|
||||
description:
|
||||
"When enabled, Markdown table rows will not have links added to them.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "ignoreCase",
|
||||
group: "Formatting",
|
||||
name: "Ignore case",
|
||||
description:
|
||||
"When enabled, link matching will be case-insensitive. The original case of the text will be preserved in the link.",
|
||||
control: "toggle",
|
||||
refreshesIndex: true,
|
||||
},
|
||||
{
|
||||
key: "matchSentenceCase",
|
||||
group: "Formatting",
|
||||
name: "Match sentence case",
|
||||
description:
|
||||
"When 'Ignore case' is OFF, match text that is capitalized at the start of a sentence. For example, 'My name' at a sentence start will match the file 'my name'. This setting is ignored when 'Ignore case' is ON.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "preventSelfLinking",
|
||||
group: "Formatting",
|
||||
name: "Prevent self-linking",
|
||||
description:
|
||||
"When enabled, text will not be linked to its own file. For example, the word 'VPN' inside VPN.md will not be converted to a link.",
|
||||
control: "toggle",
|
||||
refreshesIndex: true,
|
||||
},
|
||||
{
|
||||
key: "excludeDirsFromAutoLinking",
|
||||
group: "Formatting",
|
||||
name: "Exclude directories from automatic linking",
|
||||
description:
|
||||
"Directories to be excluded from automatic linking, one per line (e.g. 'Templates')",
|
||||
control: "textarea",
|
||||
placeholder: "Templates\nArchive",
|
||||
multiline: true,
|
||||
refreshesIndex: true,
|
||||
},
|
||||
{
|
||||
key: "removeAliasInDirs",
|
||||
group: "Formatting",
|
||||
name: "Remove aliases in directories",
|
||||
description:
|
||||
"Directories where link aliases should be removed, one per line (e.g. 'dir' will convert [[dir/xxx|yyy]] to [[dir/xxx]]). This affects both auto-generated aliases and frontmatter aliases.",
|
||||
control: "textarea",
|
||||
placeholder: "dir1\ndir2/subdir",
|
||||
multiline: true,
|
||||
refreshesIndex: true,
|
||||
},
|
||||
{
|
||||
key: "runLinterAfterFormatting",
|
||||
group: "Integrations",
|
||||
name: "Run Obsidian Linter after formatting",
|
||||
description:
|
||||
"When enabled, Obsidian Linter will be executed after Automatic Linker formatting. This requires the Obsidian Linter plugin to be installed.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "runPrettierAfterFormatting",
|
||||
group: "Integrations",
|
||||
name: "Run Prettier after formatting",
|
||||
description:
|
||||
"When enabled, Prettier will be executed after Automatic Linker formatting. This requires prettier-format plugin to be installed. https://github.com/dylanarmstrong/obsidian-prettier-plugin",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "formatDelayMs",
|
||||
group: "Integrations",
|
||||
name: "Format delay (ms)",
|
||||
description:
|
||||
"Delay in milliseconds before formatting. Increase this value if the linter/prettier runs before the file is fully saved.",
|
||||
control: "text",
|
||||
placeholder: "e.g. 100",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "replaceUrlWithTitle",
|
||||
group: "URL Replacement with Title",
|
||||
name: "Replace URL with title",
|
||||
description:
|
||||
"When enabled, raw URLs will be replaced with [Page Title](URL). Requires fetching the URL content.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "replaceUrlWithTitleIgnoreDomains",
|
||||
group: "URL Replacement with Title",
|
||||
name: "Ignore domains",
|
||||
description:
|
||||
"Ignore domains for replacing URLs with titles, one per line (e.g. x.com)",
|
||||
control: "textarea",
|
||||
placeholder: "",
|
||||
multiline: true,
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "formatGitHubURLs",
|
||||
group: "URL Formatting for GitHub",
|
||||
name: "Format GitHub URLs on save",
|
||||
description:
|
||||
"When enabled, GitHub URLs will be formatted when saving the file.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "githubEnterpriseURLs",
|
||||
group: "URL Formatting for GitHub",
|
||||
name: "GitHub Enterprise URLs",
|
||||
description:
|
||||
"Add your GitHub Enterprise URLs, one per line (e.g. github.enterprise.com)",
|
||||
control: "textarea",
|
||||
placeholder: "github.enterprise.com\ngithub.company.com",
|
||||
multiline: true,
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "formatJiraURLs",
|
||||
group: "URL Formatting for Jira",
|
||||
name: "Format JIRA URLs on save",
|
||||
description:
|
||||
"When enabled, JIRA URLs will be formatted when saving the file.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "jiraURLs",
|
||||
group: "URL Formatting for Jira",
|
||||
name: "JIRA URLs",
|
||||
description:
|
||||
"Add your JIRA URLs, one per line (e.g. jira.enterprise.com)",
|
||||
control: "textarea",
|
||||
placeholder: "jira.enterprise.com\njira.company.com",
|
||||
multiline: true,
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "formatLinearURLs",
|
||||
group: "URL Formatting for Linear",
|
||||
name: "Format Linear URLs on save",
|
||||
description:
|
||||
"When enabled, Linear URLs will be formatted when saving the file.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "showNotice",
|
||||
group: "Debug",
|
||||
name: "Show load notice",
|
||||
description: "Display a notice when markdown files are loaded.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "debug",
|
||||
group: "Debug",
|
||||
name: "Debug mode",
|
||||
description:
|
||||
"When enabled, debug information will be logged to the console.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "aiEnabled",
|
||||
group: "AI Link Enhancement (Beta)",
|
||||
name: "Enable AI Link Enhancement",
|
||||
description:
|
||||
"When enabled, an AI-powered link enhancer command will be available. It uses a local LLM to resolve ambiguous links and correct existing ones.",
|
||||
control: "toggle",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "aiEndpoint",
|
||||
group: "AI Link Enhancement (Beta)",
|
||||
name: "AI API Endpoint",
|
||||
description:
|
||||
"The URL of your OpenAI-compatible AI server (e.g. LM Studio, Ollama).",
|
||||
control: "text",
|
||||
placeholder: "http://localhost:1234/v1",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "aiModel",
|
||||
group: "AI Link Enhancement (Beta)",
|
||||
name: "AI Model",
|
||||
description: "The name of the model to use (e.g. gemma-4-7b).",
|
||||
control: "text",
|
||||
placeholder: "gemma-4-7b",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
{
|
||||
key: "aiMaxContext",
|
||||
group: "AI Link Enhancement (Beta)",
|
||||
name: "Max Context Length",
|
||||
description:
|
||||
"Number of characters around the link to provide as context to the AI.",
|
||||
control: "text",
|
||||
placeholder: "500",
|
||||
refreshesIndex: false,
|
||||
},
|
||||
] as const satisfies readonly SettingCatalogEntry[]
|
||||
|
||||
export const settingRefreshesIndex = (
|
||||
key: keyof AutomaticLinkerSettings,
|
||||
): boolean => SETTINGS_CATALOG.find(entry => entry.key === key)?.refreshesIndex ?? false
|
||||
|
||||
export const projectReplaceLinksSettings = (
|
||||
settings: AutomaticLinkerSettings,
|
||||
baseDir?: string,
|
||||
): ReplaceLinksSettings => ({
|
||||
proximityBasedLinking: settings.proximityBasedLinking,
|
||||
baseDir,
|
||||
ignoreDateFormats: settings.ignoreDateFormats,
|
||||
ignoreCase: settings.ignoreCase,
|
||||
matchSentenceCase: settings.matchSentenceCase,
|
||||
preventSelfLinking: settings.preventSelfLinking,
|
||||
removeAliasInDirs: settings.removeAliasInDirs,
|
||||
ignoreHeadings: settings.ignoreHeadings,
|
||||
ignoreMarkdownTables: settings.ignoreMarkdownTables,
|
||||
})
|
||||
|
||||
export const projectUrlFormattingSettings = (
|
||||
settings: AutomaticLinkerSettings,
|
||||
): Pick<
|
||||
AutomaticLinkerSettings,
|
||||
| "formatGitHubURLs"
|
||||
| "githubEnterpriseURLs"
|
||||
| "formatJiraURLs"
|
||||
| "jiraURLs"
|
||||
| "formatLinearURLs"
|
||||
> => ({
|
||||
formatGitHubURLs: settings.formatGitHubURLs,
|
||||
githubEnterpriseURLs: settings.githubEnterpriseURLs,
|
||||
formatJiraURLs: settings.formatJiraURLs,
|
||||
jiraURLs: settings.jiraURLs,
|
||||
formatLinearURLs: settings.formatLinearURLs,
|
||||
})
|
||||
|
|
@ -1,61 +1,2 @@
|
|||
export type AutomaticLinkerSettings = {
|
||||
formatOnSave: boolean
|
||||
showNotice: boolean
|
||||
respectNewFileFolderPath: boolean // Respect Obsidian's "Folder to create new notes in" setting as base directory
|
||||
includeAliases: boolean // Include aliases when linking
|
||||
proximityBasedLinking: boolean // Automatically resolve namespaces for shorthand links
|
||||
ignoreDateFormats: boolean // Ignore date formatted links (e.g. 2025-02-10)
|
||||
ignoreHeadings: boolean // Ignore headings (lines starting with #) when adding links
|
||||
formatGitHubURLs: boolean // Format GitHub URLs on save
|
||||
githubEnterpriseURLs: string[] // List of GitHub Enterprise URLs
|
||||
formatJiraURLs: boolean // Format Jira URLs on save
|
||||
jiraURLs: string[] // List of Jira URLs (domains)
|
||||
formatLinearURLs: boolean // Format Linear URLs on save
|
||||
debug: boolean // Enable debug logging
|
||||
ignoreCase: boolean // Ignore case when matching links
|
||||
matchSentenceCase: boolean // Match sentence-start capitalized text when ignoreCase is off
|
||||
replaceUrlWithTitle: boolean // Replace raw URLs with [Title](URL)
|
||||
replaceUrlWithTitleIgnoreDomains: string[] // List of domains to ignore when replacing URLs with titles
|
||||
excludeDirsFromAutoLinking: string[] // Optional: List of directories to exclude from auto-linking
|
||||
preventSelfLinking: boolean // Prevent linking text to its own file
|
||||
removeAliasInDirs: string[] // Remove aliases for links in specified directories
|
||||
ignoreMarkdownTables: boolean // Ignore Markdown table rows when adding links
|
||||
runLinterAfterFormatting: boolean // Run Obsidian Linter after automatic linker formatting
|
||||
runPrettierAfterFormatting: boolean // Run Prettier after automatic linker formatting
|
||||
formatDelayMs: number // Delay in milliseconds before running linter after formatting
|
||||
aiEnabled: boolean // Enable AI link enhancement
|
||||
aiEndpoint: string // API endpoint for AI (OpenAI compatible)
|
||||
aiModel: string // Model name for AI
|
||||
aiMaxContext: number // Maximum context length for AI
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: AutomaticLinkerSettings = {
|
||||
formatOnSave: false,
|
||||
showNotice: false,
|
||||
respectNewFileFolderPath: true, // Default: do not respect Obsidian's "Folder to create new notes in" setting
|
||||
includeAliases: true, // Default: include aliases
|
||||
proximityBasedLinking: true, // Default: enable automatic namespace resolution
|
||||
ignoreDateFormats: true, // Default: ignore date formats (e.g. 2025-02-10)
|
||||
ignoreHeadings: false, // Default: do not ignore headings
|
||||
formatGitHubURLs: true, // Default: format GitHub URLs
|
||||
githubEnterpriseURLs: [], // Default: empty list
|
||||
formatJiraURLs: true, // Default: format Jira URLs
|
||||
jiraURLs: [], // Default: empty list
|
||||
formatLinearURLs: false, // Default: format Linear URLs
|
||||
debug: false, // Default: disable debug logging
|
||||
ignoreCase: true, // Default: case-sensitive matching
|
||||
matchSentenceCase: true, // Default: match sentence-start capitalized text
|
||||
replaceUrlWithTitle: true, // Default: enable replacing URLs with titles
|
||||
replaceUrlWithTitleIgnoreDomains: [],
|
||||
excludeDirsFromAutoLinking: [], // Default: no excluded directories
|
||||
preventSelfLinking: false, // Default: allow self-linking (backward compatibility)
|
||||
removeAliasInDirs: [], // Default: no directories for alias removal
|
||||
ignoreMarkdownTables: false, // Default: add links inside Markdown tables
|
||||
runLinterAfterFormatting: false, // Default: do not run linter after formatting
|
||||
runPrettierAfterFormatting: false, // Run Prettier after automatic linker formatting
|
||||
formatDelayMs: 1, // Default: 1ms delay before running linter
|
||||
aiEnabled: false, // Default: disable AI enhancement
|
||||
aiEndpoint: "http://localhost:1234/v1", // Default: LM Studio
|
||||
aiModel: "gemma-4-7b", // Default: Gemma 4
|
||||
aiMaxContext: 500, // Default: 500 characters
|
||||
}
|
||||
export type { AutomaticLinkerSettings } from "./settings-catalog"
|
||||
export { DEFAULT_SETTINGS } from "./settings-catalog"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import { App, PluginSettingTab, Setting } from "obsidian"
|
||||
import AutomaticLinkerPlugin from "../main"
|
||||
import {
|
||||
AutomaticLinkerSettings,
|
||||
SETTINGS_CATALOG,
|
||||
SettingCatalogEntry,
|
||||
settingRefreshesIndex,
|
||||
} from "./settings-catalog"
|
||||
|
||||
export class AutomaticLinkerPluginSettingsTab extends PluginSettingTab {
|
||||
plugin: AutomaticLinkerPlugin
|
||||
|
|
@ -8,500 +14,107 @@ export class AutomaticLinkerPluginSettingsTab extends PluginSettingTab {
|
|||
this.plugin = plugin
|
||||
}
|
||||
|
||||
private async setSettingValue<K extends keyof AutomaticLinkerSettings>(
|
||||
key: K,
|
||||
value: AutomaticLinkerSettings[K],
|
||||
) {
|
||||
this.plugin.settings[key] = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
if (key === "replaceUrlWithTitleIgnoreDomains") {
|
||||
await this.plugin.buildUrlTitleMap()
|
||||
}
|
||||
if (settingRefreshesIndex(key)) {
|
||||
this.plugin.refreshFileDataAndTrie()
|
||||
}
|
||||
}
|
||||
|
||||
private parseTextValue<K extends keyof AutomaticLinkerSettings>(
|
||||
key: K,
|
||||
currentValue: AutomaticLinkerSettings[K],
|
||||
nextValue: string,
|
||||
): AutomaticLinkerSettings[K] | null {
|
||||
if (typeof currentValue !== "number") {
|
||||
return nextValue as AutomaticLinkerSettings[K]
|
||||
}
|
||||
|
||||
const parsedValue = parseInt(nextValue)
|
||||
if (isNaN(parsedValue)) {
|
||||
return null
|
||||
}
|
||||
if (key === "formatDelayMs" && parsedValue < 0) {
|
||||
return null
|
||||
}
|
||||
if (key === "aiMaxContext" && parsedValue <= 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return parsedValue as AutomaticLinkerSettings[K]
|
||||
}
|
||||
|
||||
private renderSetting(containerEl: HTMLElement, entry: SettingCatalogEntry) {
|
||||
const setting = new Setting(containerEl)
|
||||
.setName(entry.name)
|
||||
.setDesc(entry.description)
|
||||
const value = this.plugin.settings[entry.key]
|
||||
|
||||
if (entry.control === "toggle") {
|
||||
setting.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(Boolean(value))
|
||||
.onChange(async (nextValue) => {
|
||||
await this.setSettingValue(entry.key, nextValue as never)
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (entry.control === "text") {
|
||||
setting.addText((text) => {
|
||||
text.setPlaceholder(entry.placeholder ?? "")
|
||||
.setValue(String(value))
|
||||
.onChange(async (nextValue) => {
|
||||
const parsedValue = this.parseTextValue(
|
||||
entry.key,
|
||||
value,
|
||||
nextValue,
|
||||
)
|
||||
if (parsedValue === null) {
|
||||
return
|
||||
}
|
||||
await this.setSettingValue(entry.key, parsedValue)
|
||||
})
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setting.addTextArea((text) => {
|
||||
text.setPlaceholder(entry.placeholder ?? "")
|
||||
.setValue(Array.isArray(value) ? value.join("\n") : String(value))
|
||||
.onChange(async (nextValue) => {
|
||||
await this.setSettingValue(
|
||||
entry.key,
|
||||
nextValue
|
||||
.split("\n")
|
||||
.map(item => item.trim())
|
||||
.filter(Boolean) as never,
|
||||
)
|
||||
})
|
||||
text.inputEl.rows = 4
|
||||
text.inputEl.cols = 50
|
||||
})
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this
|
||||
containerEl.empty()
|
||||
|
||||
new Setting(containerEl).setName("Formatting").setHeading()
|
||||
|
||||
// Toggle for "Format on Save" setting.
|
||||
new Setting(containerEl)
|
||||
.setName("Format on save")
|
||||
.setDesc(
|
||||
"When enabled, the file will be automatically formatted (links replaced) when saving.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.formatOnSave)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.formatOnSave = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for respecting Obsidian's "Folder to create new notes in" setting
|
||||
new Setting(containerEl)
|
||||
.setName("Respect 'Folder to create new notes in' setting")
|
||||
.setDesc(
|
||||
"When enabled, the plugin will use Obsidian's 'Folder to create new notes in' setting as the base directory for omitting folder prefixes in links.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.respectNewFileFolderPath)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.respectNewFileFolderPath = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
this.plugin.refreshFileDataAndTrie()
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for including aliases.
|
||||
new Setting(containerEl)
|
||||
.setName("Include aliases")
|
||||
.setDesc(
|
||||
"When enabled, aliases will be included when processing links. Note: A restart is required for changes to take effect.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.includeAliases)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.includeAliases = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
this.plugin.refreshFileDataAndTrie()
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for proximity-based linking.
|
||||
new Setting(containerEl)
|
||||
.setName("Proximity-based linking")
|
||||
.setDesc(
|
||||
"When enabled, the plugin will automatically resolve namespaces for shorthand links. If multiple candidates share the same shorthand, the candidate with the most common path segments relative to the current file will be selected.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.proximityBasedLinking)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.proximityBasedLinking = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
this.plugin.refreshFileDataAndTrie()
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for ignoring date formats.
|
||||
new Setting(containerEl)
|
||||
.setName("Ignore date formats")
|
||||
.setDesc(
|
||||
"When enabled, links that match date formats (e.g. 2025-02-10) will be ignored. This helps maintain compatibility with Obsidian Tasks.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.ignoreDateFormats)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ignoreDateFormats = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
this.plugin.refreshFileDataAndTrie()
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for ignoring headings
|
||||
new Setting(containerEl)
|
||||
.setName("Ignore headings")
|
||||
.setDesc(
|
||||
"When enabled, headings (lines starting with #) will not have links added to them.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.ignoreHeadings)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ignoreHeadings = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for ignoring Markdown tables
|
||||
new Setting(containerEl)
|
||||
.setName("Ignore Markdown tables")
|
||||
.setDesc(
|
||||
"When enabled, Markdown table rows will not have links added to them.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.ignoreMarkdownTables)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ignoreMarkdownTables = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for ignoring case in link matching
|
||||
new Setting(containerEl)
|
||||
.setName("Ignore case")
|
||||
.setDesc(
|
||||
"When enabled, link matching will be case-insensitive. The original case of the text will be preserved in the link.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.ignoreCase)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.ignoreCase = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
this.plugin.refreshFileDataAndTrie()
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for matching sentence-case text
|
||||
new Setting(containerEl)
|
||||
.setName("Match sentence case")
|
||||
.setDesc(
|
||||
"When 'Ignore case' is OFF, match text that is capitalized at the start of a sentence. For example, 'My name' at a sentence start will match the file 'my name'. This setting is ignored when 'Ignore case' is ON.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.matchSentenceCase)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.matchSentenceCase = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for preventing self-linking
|
||||
new Setting(containerEl)
|
||||
.setName("Prevent self-linking")
|
||||
.setDesc(
|
||||
"When enabled, text will not be linked to its own file. For example, the word 'VPN' inside VPN.md will not be converted to a link.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.preventSelfLinking)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.preventSelfLinking = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
this.plugin.refreshFileDataAndTrie()
|
||||
})
|
||||
})
|
||||
|
||||
// Add excluding dirs that you wish to exclude from the automatic linking
|
||||
new Setting(containerEl)
|
||||
.setName("Exclude directories from automatic linking")
|
||||
.setDesc(
|
||||
"Directories to be excluded from automatic linking, one per line (e.g. 'Templates')",
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
text.setPlaceholder("Templates\nArchive")
|
||||
.setValue(
|
||||
this.plugin.settings.excludeDirsFromAutoLinking.join(
|
||||
"\n",
|
||||
),
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
// Split by newlines and filter out empty lines
|
||||
const dirs = value.split("\n").map(dir => dir.trim()).filter(Boolean)
|
||||
this.plugin.settings.excludeDirsFromAutoLinking = dirs
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
this.plugin.refreshFileDataAndTrie()
|
||||
})
|
||||
})
|
||||
|
||||
// Remove aliases for links in specified directories
|
||||
new Setting(containerEl)
|
||||
.setName("Remove aliases in directories")
|
||||
.setDesc(
|
||||
"Directories where link aliases should be removed, one per line (e.g. 'dir' will convert [[dir/xxx|yyy]] to [[dir/xxx]]). This affects both auto-generated aliases and frontmatter aliases.",
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
text.setPlaceholder("dir1\ndir2/subdir")
|
||||
.setValue(this.plugin.settings.removeAliasInDirs.join("\n"))
|
||||
.onChange(async (value) => {
|
||||
// Split by newlines and filter out empty lines
|
||||
const dirs = value
|
||||
.split("\n")
|
||||
.map(dir => dir.trim())
|
||||
.filter(Boolean)
|
||||
this.plugin.settings.removeAliasInDirs = dirs
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
this.plugin.refreshFileDataAndTrie()
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Integrations")
|
||||
.setHeading()
|
||||
|
||||
// Toggle for running linter after formatting
|
||||
new Setting(containerEl)
|
||||
.setName("Run Obsidian Linter after formatting")
|
||||
.setDesc(
|
||||
"When enabled, Obsidian Linter will be executed after Automatic Linker formatting. This requires the Obsidian Linter plugin to be installed.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.runLinterAfterFormatting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.runLinterAfterFormatting = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for running prettier after formatting
|
||||
new Setting(containerEl)
|
||||
.setName("Run Prettier after formatting")
|
||||
.setDesc(
|
||||
"When enabled, Prettier will be executed after Automatic Linker formatting. This requires prettier-format plugin to be installed. https://github.com/dylanarmstrong/obsidian-prettier-plugin",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.runPrettierAfterFormatting)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.runPrettierAfterFormatting = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
// Setting for linter delay
|
||||
new Setting(containerEl)
|
||||
.setName("Format delay (ms)")
|
||||
.setDesc(
|
||||
"Delay in milliseconds before formatting. Increase this value if the linter/prettier runs before the file is fully saved.",
|
||||
)
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("e.g. 100")
|
||||
.setValue(this.plugin.settings.formatDelayMs.toString())
|
||||
.onChange(async (value) => {
|
||||
const parsedValue = parseInt(value)
|
||||
if (!isNaN(parsedValue) && parsedValue >= 0) {
|
||||
this.plugin.settings.formatDelayMs = parsedValue
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("URL Replacement with Title")
|
||||
.setHeading()
|
||||
|
||||
// Toggle for replacing URLs with titles
|
||||
new Setting(containerEl)
|
||||
.setName("Replace URL with title")
|
||||
.setDesc(
|
||||
"When enabled, raw URLs will be replaced with [Page Title](URL). Requires fetching the URL content.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.replaceUrlWithTitle)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.replaceUrlWithTitle = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Ignore domains")
|
||||
.setDesc(
|
||||
"Ignore domains for replacing URLs with titles, one per line (e.g. x.com)",
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
text.setPlaceholder("")
|
||||
.setValue(
|
||||
this.plugin.settings.replaceUrlWithTitleIgnoreDomains.join(
|
||||
"\n",
|
||||
),
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
// Split by newlines and filter out empty lines
|
||||
const urls = value
|
||||
.split("\n")
|
||||
.map(url => url.trim())
|
||||
.filter(Boolean)
|
||||
this.plugin.settings.replaceUrlWithTitleIgnoreDomains
|
||||
= urls
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
await this.plugin.buildUrlTitleMap()
|
||||
})
|
||||
// Make the text area taller
|
||||
text.inputEl.rows = 4
|
||||
text.inputEl.cols = 50
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("URL Formatting for GitHub")
|
||||
.setHeading()
|
||||
|
||||
// Toggle for formatting GitHub URLs on save
|
||||
new Setting(containerEl)
|
||||
.setName("Format GitHub URLs on save")
|
||||
.setDesc(
|
||||
"When enabled, GitHub URLs will be formatted when saving the file.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.formatGitHubURLs)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.formatGitHubURLs = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
// Add GitHub Enterprise URLs setting
|
||||
new Setting(containerEl)
|
||||
.setName("GitHub Enterprise URLs")
|
||||
.setDesc(
|
||||
"Add your GitHub Enterprise URLs, one per line (e.g. github.enterprise.com)",
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
text.setPlaceholder("github.enterprise.com\ngithub.company.com")
|
||||
.setValue(
|
||||
this.plugin.settings.githubEnterpriseURLs.join("\n"),
|
||||
)
|
||||
.onChange(async (value) => {
|
||||
// Split by newlines and filter out empty lines
|
||||
const urls = value
|
||||
.split("\n")
|
||||
.map(url => url.trim())
|
||||
.filter(Boolean)
|
||||
this.plugin.settings.githubEnterpriseURLs = urls
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
// Make the text area taller
|
||||
text.inputEl.rows = 4
|
||||
text.inputEl.cols = 50
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("URL Formatting for Jira")
|
||||
.setHeading()
|
||||
|
||||
// Toggle for formatting JIRA URLs on save
|
||||
new Setting(containerEl)
|
||||
.setName("Format JIRA URLs on save")
|
||||
.setDesc(
|
||||
"When enabled, JIRA URLs will be formatted when saving the file.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.formatJiraURLs)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.formatJiraURLs = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
// Add JIRA URLs setting
|
||||
new Setting(containerEl)
|
||||
.setName("JIRA URLs")
|
||||
.setDesc(
|
||||
"Add your JIRA URLs, one per line (e.g. jira.enterprise.com)",
|
||||
)
|
||||
.addTextArea((text) => {
|
||||
text.setPlaceholder("jira.enterprise.com\njira.company.com")
|
||||
.setValue(this.plugin.settings.jiraURLs.join("\n"))
|
||||
.onChange(async (value) => {
|
||||
// Split by newlines and filter out empty lines
|
||||
const urls = value
|
||||
.split("\n")
|
||||
.map(url => url.trim())
|
||||
.filter(Boolean)
|
||||
this.plugin.settings.jiraURLs = urls
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
// Make the text area taller
|
||||
text.inputEl.rows = 4
|
||||
text.inputEl.cols = 50
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("URL Formatting for Linear")
|
||||
.setHeading()
|
||||
|
||||
// Toggle for formatting Linear URLs on save
|
||||
new Setting(containerEl)
|
||||
.setName("Format Linear URLs on save")
|
||||
.setDesc(
|
||||
"When enabled, Linear URLs will be formatted when saving the file.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.formatLinearURLs)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.formatLinearURLs = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl).setName("Debug").setHeading()
|
||||
|
||||
// Toggle for showing the load notice.
|
||||
new Setting(containerEl)
|
||||
.setName("Show load notice")
|
||||
.setDesc("Display a notice when markdown files are loaded.")
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.showNotice)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.showNotice = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
// Toggle for debug logging
|
||||
new Setting(containerEl)
|
||||
.setName("Debug mode")
|
||||
.setDesc(
|
||||
"When enabled, debug information will be logged to the console.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.debug)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.debug = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("AI Link Enhancement (Beta)")
|
||||
.setHeading()
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Enable AI Link Enhancement")
|
||||
.setDesc(
|
||||
"When enabled, an AI-powered link enhancer command will be available. It uses a local LLM to resolve ambiguous links and correct existing ones.",
|
||||
)
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.aiEnabled)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.aiEnabled = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("AI API Endpoint")
|
||||
.setDesc("The URL of your OpenAI-compatible AI server (e.g. LM Studio, Ollama).")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("http://localhost:1234/v1")
|
||||
.setValue(this.plugin.settings.aiEndpoint)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.aiEndpoint = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("AI Model")
|
||||
.setDesc("The name of the model to use (e.g. gemma-4-7b).")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("gemma-4-7b")
|
||||
.setValue(this.plugin.settings.aiModel)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.aiModel = value
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
})
|
||||
})
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName("Max Context Length")
|
||||
.setDesc("Number of characters around the link to provide as context to the AI.")
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("500")
|
||||
.setValue(this.plugin.settings.aiMaxContext.toString())
|
||||
.onChange(async (value) => {
|
||||
const parsedValue = parseInt(value)
|
||||
if (!isNaN(parsedValue) && parsedValue > 0) {
|
||||
this.plugin.settings.aiMaxContext = parsedValue
|
||||
await this.plugin.saveData(this.plugin.settings)
|
||||
}
|
||||
})
|
||||
})
|
||||
const renderedGroups = new Set<string>()
|
||||
for (const entry of SETTINGS_CATALOG) {
|
||||
if (!renderedGroups.has(entry.group)) {
|
||||
new Setting(containerEl).setName(entry.group).setHeading()
|
||||
renderedGroups.add(entry.group)
|
||||
}
|
||||
this.renderSetting(containerEl, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue