mirror of
https://github.com/dragonish/obsidian-heading-decorator.git
synced 2026-07-22 05:42:05 +00:00
feat(editor): add gutter-based heading decorator renderer
Add an optional gutter column renderer as an alternative to inline Decoration.widget placement. When enabled, heading decorations render in a separate CodeMirror gutter outside the document flow, preserving text alignment. - Extract shared counter creation into common/counter-factory.ts - Add HeadingGutterMarker (GutterMarker) and createHeadingGutterExtension - Support toggling between inline and gutter modes at runtime via in-place extension swap (workspace.updateOptions pattern) - Add useGutter toggle and gutterPosition dropdown to settings UI - Add i18n strings for en, zh, zh-TW - Add gutter CSS with opacity variants and table-cell hiding
This commit is contained in:
parent
2af9551ea8
commit
7d21665b7f
11 changed files with 430 additions and 123 deletions
123
common/counter-factory.ts
Normal file
123
common/counter-factory.ts
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import type { HeadingDecoratorSettings } from "./data";
|
||||
import { getUnorderedLevelHeadings, getOrderedCustomIdents } from "./data";
|
||||
import {
|
||||
Querier,
|
||||
UnorderedCounter,
|
||||
OrderedCounter,
|
||||
IndependentCounter,
|
||||
SpliceCounter,
|
||||
} from "./counter";
|
||||
import { Heading } from "./heading";
|
||||
|
||||
interface DocumentLineReader {
|
||||
lines: number;
|
||||
line(n: number): { text: string };
|
||||
}
|
||||
|
||||
export function createCounterFromSettings(
|
||||
settings: HeadingDecoratorSettings,
|
||||
doc: DocumentLineReader
|
||||
): Counter {
|
||||
const {
|
||||
decoratorMode = "orderd",
|
||||
maxRecLevel,
|
||||
orderedStyleType,
|
||||
orderedDelimiter,
|
||||
orderedTrailingDelimiter,
|
||||
orderedCustomTrailingDelimiter,
|
||||
orderedLeadingDelimiter,
|
||||
orderedCustomLeadingDelimiter,
|
||||
orderedCustomIdents,
|
||||
orderedSpecifiedString,
|
||||
orderedAlwaysIgnore,
|
||||
orderedIgnoreSingle,
|
||||
orderedIgnoreMaximum = 6,
|
||||
orderedBasedOnExisting,
|
||||
orderedAllowZeroLevel,
|
||||
unorderedLevelHeadings,
|
||||
independentSettings,
|
||||
spliceSettings,
|
||||
} = settings;
|
||||
|
||||
if (decoratorMode === "unordered") {
|
||||
return new UnorderedCounter(
|
||||
getUnorderedLevelHeadings(unorderedLevelHeadings),
|
||||
maxRecLevel
|
||||
);
|
||||
}
|
||||
|
||||
let ignoreTopLevel = 0;
|
||||
const ignoreSingle = !orderedAlwaysIgnore && orderedIgnoreSingle;
|
||||
const ignoreLimit = orderedAlwaysIgnore ? orderedIgnoreMaximum : 0;
|
||||
if (ignoreSingle || orderedBasedOnExisting) {
|
||||
const querier = new Querier(orderedAllowZeroLevel, maxRecLevel);
|
||||
const heading = new Heading();
|
||||
for (let lineIndex = 1; lineIndex <= doc.lines; lineIndex++) {
|
||||
const line = doc.line(lineIndex);
|
||||
const lineText = line.text;
|
||||
const nextLineIndex = lineIndex + 1;
|
||||
const nextLineText =
|
||||
nextLineIndex <= doc.lines ? doc.line(nextLineIndex).text : "";
|
||||
const level = heading.handler(lineIndex, lineText, nextLineText);
|
||||
if (level === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
querier.handler(level);
|
||||
|
||||
ignoreTopLevel = querier.query(ignoreSingle, orderedIgnoreMaximum);
|
||||
if (ignoreTopLevel <= ignoreLimit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ignoreTopLevel < ignoreLimit) {
|
||||
ignoreTopLevel = ignoreLimit;
|
||||
}
|
||||
|
||||
if (decoratorMode === "independent") {
|
||||
return new IndependentCounter({
|
||||
maxRecLevel,
|
||||
ignoreTopLevel,
|
||||
allowZeroLevel: orderedAllowZeroLevel,
|
||||
orderedRecLevel: independentSettings?.orderedRecLevel,
|
||||
h1: independentSettings?.h1,
|
||||
h2: independentSettings?.h2,
|
||||
h3: independentSettings?.h3,
|
||||
h4: independentSettings?.h4,
|
||||
h5: independentSettings?.h5,
|
||||
h6: independentSettings?.h6,
|
||||
});
|
||||
} else if (decoratorMode === "splice") {
|
||||
return new SpliceCounter({
|
||||
maxRecLevel,
|
||||
ignoreTopLevel,
|
||||
allowZeroLevel: orderedAllowZeroLevel,
|
||||
delimiter: spliceSettings?.delimiter,
|
||||
trailingDelimiter: spliceSettings?.trailingDelimiter,
|
||||
customTrailingDelimiter: spliceSettings?.customTrailingDelimiter,
|
||||
leadingDelimiter: spliceSettings?.leadingDelimiter,
|
||||
customLeadingDelimiter: spliceSettings?.customLeadingDelimiter,
|
||||
h1: spliceSettings?.h1,
|
||||
h2: spliceSettings?.h2,
|
||||
h3: spliceSettings?.h3,
|
||||
h4: spliceSettings?.h4,
|
||||
h5: spliceSettings?.h5,
|
||||
h6: spliceSettings?.h6,
|
||||
});
|
||||
} else {
|
||||
return new OrderedCounter({
|
||||
maxRecLevel,
|
||||
ignoreTopLevel,
|
||||
allowZeroLevel: orderedAllowZeroLevel,
|
||||
styleType: orderedStyleType,
|
||||
delimiter: orderedDelimiter,
|
||||
trailingDelimiter: orderedTrailingDelimiter,
|
||||
customTrailingDelimiter: orderedCustomTrailingDelimiter,
|
||||
leadingDelimiter: orderedLeadingDelimiter,
|
||||
customLeadingDelimiter: orderedCustomLeadingDelimiter,
|
||||
customIdents: getOrderedCustomIdents(orderedCustomIdents),
|
||||
specifiedString: orderedSpecifiedString,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -140,6 +140,8 @@ export type HeadingPluginSettings = {
|
|||
enabledInSource: boolean;
|
||||
enabledSourceSettings: boolean;
|
||||
sourceHideNumberSigns: boolean;
|
||||
useGutter: boolean;
|
||||
gutterPosition: GutterPosition;
|
||||
enabledInOutline: boolean;
|
||||
enabledOutlineSettings: boolean;
|
||||
enabledInQuietOutline: boolean;
|
||||
|
|
@ -160,6 +162,8 @@ export type HeadingPluginData = Omit<
|
|||
| "readingSettings"
|
||||
| "enabledPreviewSettings"
|
||||
| "enabledSourceSettings"
|
||||
| "useGutter"
|
||||
| "gutterPosition"
|
||||
| "enabledInOutline"
|
||||
| "outlineSettings"
|
||||
| "enabledInQuietOutline"
|
||||
|
|
@ -182,6 +186,8 @@ export const className = {
|
|||
quietOutlineContainer: "quiet-outline-custom-heading-container",
|
||||
fileExplorer: "file-explorer-custom-heading-decorator",
|
||||
fileExplorerContainer: "file-explorer-custom-heading-container",
|
||||
gutter: "heading-decorator-gutter",
|
||||
gutterMarker: "heading-decorator-gutter-marker",
|
||||
before: "before-heading-decorator",
|
||||
beforeInside: "before-inside-heading-decorator",
|
||||
after: "after-heading-decorator",
|
||||
|
|
@ -313,6 +319,8 @@ export function defaultSettings(): HeadingPluginSettings {
|
|||
enabledSourceSettings: false,
|
||||
sourceSettings: defaultHeadingDecoratorSettings(),
|
||||
sourceHideNumberSigns: false,
|
||||
useGutter: false,
|
||||
gutterPosition: "before-line-numbers",
|
||||
enabledInOutline: false,
|
||||
enabledOutlineSettings: false,
|
||||
outlineSettings: defaultHeadingDecoratorSettings(),
|
||||
|
|
|
|||
142
components/editor-gutter.ts
Normal file
142
components/editor-gutter.ts
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
import { editorLivePreviewField } from "obsidian";
|
||||
import {
|
||||
ViewPlugin,
|
||||
ViewUpdate,
|
||||
EditorView,
|
||||
gutter,
|
||||
GutterMarker,
|
||||
} from "@codemirror/view";
|
||||
import { Prec, RangeSet, RangeSetBuilder } from "@codemirror/state";
|
||||
import type { HeadingPluginData } from "../common/data";
|
||||
import { className } from "../common/data";
|
||||
import { createCounterFromSettings } from "../common/counter-factory";
|
||||
import { Heading } from "../common/heading";
|
||||
import { updateEditorMode } from "./editor";
|
||||
|
||||
class HeadingGutterMarker extends GutterMarker {
|
||||
readonly content: string;
|
||||
readonly level: number;
|
||||
readonly opacity: OpacityOptions;
|
||||
|
||||
constructor(content: string, level: number, opacity: OpacityOptions) {
|
||||
super();
|
||||
this.content = content;
|
||||
this.level = level;
|
||||
this.opacity = opacity;
|
||||
}
|
||||
|
||||
toDOM(): HTMLElement {
|
||||
const div = document.createElement("div");
|
||||
div.className = className.gutterMarker;
|
||||
div.dataset.decoratorOpacity = `${this.opacity}%`;
|
||||
div.dataset.decoratorLevel = this.level.toString();
|
||||
div.textContent = this.content;
|
||||
return div;
|
||||
}
|
||||
|
||||
eq(other: HeadingGutterMarker): boolean {
|
||||
return (
|
||||
this.content === other.content &&
|
||||
this.level === other.level &&
|
||||
this.opacity === other.opacity
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function createHeadingGutterExtension(
|
||||
getPluginData: () => Promise<HeadingPluginData>,
|
||||
showBeforeLineNumbers: boolean
|
||||
): Array<ViewPlugin<HeadingGutterViewPlugin> | ReturnType<typeof Prec.high>> {
|
||||
const markers = ViewPlugin.fromClass(
|
||||
class HeadingGutterViewPlugin {
|
||||
markers: RangeSet<HeadingGutterMarker> = RangeSet.empty;
|
||||
private getPluginData: () => Promise<HeadingPluginData>;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.getPluginData = getPluginData;
|
||||
this.buildMarkers(view, view.state.field(editorLivePreviewField));
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (
|
||||
update.docChanged ||
|
||||
update.viewportChanged ||
|
||||
update.transactions.some((tr) =>
|
||||
tr.effects.some((e) => e.is(updateEditorMode))
|
||||
)
|
||||
) {
|
||||
this.buildMarkers(
|
||||
update.view,
|
||||
update.state.field(editorLivePreviewField)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async buildMarkers(
|
||||
view: EditorView,
|
||||
isLivePreviewMode: boolean
|
||||
) {
|
||||
const pluginData = await this.getPluginData();
|
||||
|
||||
const enabled =
|
||||
(isLivePreviewMode && pluginData.enabledInPreview) ||
|
||||
(!isLivePreviewMode && pluginData.enabledInSource);
|
||||
|
||||
if (!enabled) {
|
||||
this.markers = RangeSet.empty;
|
||||
view.requestMeasure();
|
||||
return;
|
||||
}
|
||||
|
||||
const doc = view.state.doc;
|
||||
const settings = isLivePreviewMode
|
||||
? pluginData.previewSettings
|
||||
: pluginData.sourceSettings;
|
||||
|
||||
const { opacity } = settings;
|
||||
const counter = createCounterFromSettings(settings, doc);
|
||||
|
||||
const builder = new RangeSetBuilder<HeadingGutterMarker>();
|
||||
const heading = new Heading();
|
||||
|
||||
for (let lineIndex = 1; lineIndex <= doc.lines; lineIndex++) {
|
||||
const line = doc.line(lineIndex);
|
||||
const lineText = line.text;
|
||||
const nextLineIndex = lineIndex + 1;
|
||||
const nextLineText =
|
||||
nextLineIndex <= doc.lines ? doc.line(nextLineIndex).text : "";
|
||||
const level = heading.handler(lineIndex, lineText, nextLineText);
|
||||
if (level === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = counter.decorator(level);
|
||||
if (content) {
|
||||
const marker = new HeadingGutterMarker(content, level, opacity);
|
||||
builder.add(line.from, line.from, marker);
|
||||
}
|
||||
}
|
||||
|
||||
this.markers = builder.finish();
|
||||
view.requestMeasure();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const gutterPrec = showBeforeLineNumbers ? Prec.high : Prec.low;
|
||||
return [
|
||||
markers,
|
||||
gutterPrec(
|
||||
gutter({
|
||||
class: className.gutter,
|
||||
markers(view) {
|
||||
return view.plugin(markers)?.markers ?? RangeSet.empty;
|
||||
},
|
||||
})
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
type HeadingGutterViewPlugin = {
|
||||
markers: RangeSet<HeadingGutterMarker>;
|
||||
};
|
||||
|
|
@ -9,19 +9,8 @@ import {
|
|||
import { RangeSetBuilder, StateEffect, StateField } from "@codemirror/state";
|
||||
import { HeadingWidget } from "./weight";
|
||||
import type { HeadingPluginData } from "../common/data";
|
||||
import {
|
||||
className,
|
||||
getUnorderedLevelHeadings,
|
||||
getOrderedCustomIdents,
|
||||
findFirstCharacterIndex,
|
||||
} from "../common/data";
|
||||
import {
|
||||
Querier,
|
||||
UnorderedCounter,
|
||||
OrderedCounter,
|
||||
IndependentCounter,
|
||||
SpliceCounter,
|
||||
} from "../common/counter";
|
||||
import { className, findFirstCharacterIndex } from "../common/data";
|
||||
import { createCounterFromSettings } from "../common/counter-factory";
|
||||
import { Heading } from "../common/heading";
|
||||
|
||||
/** A StateEffect for updating decorations */
|
||||
|
|
@ -103,113 +92,13 @@ export class HeadingEditorViewPlugin implements PluginValue {
|
|||
const builder = new RangeSetBuilder<Decoration>();
|
||||
const doc = view.state.doc;
|
||||
|
||||
const {
|
||||
decoratorMode = "orderd",
|
||||
position,
|
||||
opacity,
|
||||
maxRecLevel,
|
||||
orderedStyleType,
|
||||
orderedDelimiter,
|
||||
orderedTrailingDelimiter,
|
||||
orderedCustomTrailingDelimiter,
|
||||
orderedLeadingDelimiter,
|
||||
orderedCustomLeadingDelimiter,
|
||||
orderedCustomIdents,
|
||||
orderedSpecifiedString,
|
||||
orderedAlwaysIgnore,
|
||||
orderedIgnoreSingle,
|
||||
orderedIgnoreMaximum = 6,
|
||||
orderedBasedOnExisting,
|
||||
orderedAllowZeroLevel,
|
||||
unorderedLevelHeadings,
|
||||
independentSettings,
|
||||
spliceSettings,
|
||||
} = isLivePreviwMode
|
||||
const settings = isLivePreviwMode
|
||||
? pluginData.previewSettings
|
||||
: pluginData.sourceSettings;
|
||||
|
||||
let counter: Counter;
|
||||
if (decoratorMode === "unordered") {
|
||||
counter = new UnorderedCounter(
|
||||
getUnorderedLevelHeadings(unorderedLevelHeadings),
|
||||
maxRecLevel
|
||||
);
|
||||
} else {
|
||||
let ignoreTopLevel = 0;
|
||||
const ignoreSingle = !orderedAlwaysIgnore && orderedIgnoreSingle;
|
||||
const ignoreLimit = orderedAlwaysIgnore ? orderedIgnoreMaximum : 0;
|
||||
if (ignoreSingle || orderedBasedOnExisting) {
|
||||
const queier = new Querier(orderedAllowZeroLevel, maxRecLevel);
|
||||
const heading = new Heading();
|
||||
for (let lineIndex = 1; lineIndex <= doc.lines; lineIndex++) {
|
||||
const line = doc.line(lineIndex);
|
||||
const lineText = line.text;
|
||||
const nextLineIndex = lineIndex + 1;
|
||||
const nextLineText =
|
||||
nextLineIndex <= doc.lines ? doc.line(nextLineIndex).text : "";
|
||||
const level = heading.handler(lineIndex, lineText, nextLineText);
|
||||
if (level === -1) {
|
||||
continue;
|
||||
}
|
||||
const { position, opacity } = settings;
|
||||
|
||||
queier.handler(level);
|
||||
|
||||
ignoreTopLevel = queier.query(ignoreSingle, orderedIgnoreMaximum);
|
||||
if (ignoreTopLevel <= ignoreLimit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ignoreTopLevel < ignoreLimit) {
|
||||
ignoreTopLevel = ignoreLimit;
|
||||
}
|
||||
|
||||
if (decoratorMode === "independent") {
|
||||
counter = new IndependentCounter({
|
||||
maxRecLevel,
|
||||
ignoreTopLevel,
|
||||
allowZeroLevel: orderedAllowZeroLevel,
|
||||
orderedRecLevel: independentSettings?.orderedRecLevel,
|
||||
h1: independentSettings?.h1,
|
||||
h2: independentSettings?.h2,
|
||||
h3: independentSettings?.h3,
|
||||
h4: independentSettings?.h4,
|
||||
h5: independentSettings?.h5,
|
||||
h6: independentSettings?.h6,
|
||||
});
|
||||
} else if (decoratorMode === "splice") {
|
||||
counter = new SpliceCounter({
|
||||
maxRecLevel,
|
||||
ignoreTopLevel,
|
||||
allowZeroLevel: orderedAllowZeroLevel,
|
||||
delimiter: spliceSettings?.delimiter,
|
||||
trailingDelimiter: spliceSettings?.trailingDelimiter,
|
||||
customTrailingDelimiter: spliceSettings?.customTrailingDelimiter,
|
||||
leadingDelimiter: spliceSettings?.leadingDelimiter,
|
||||
customLeadingDelimiter: spliceSettings?.customLeadingDelimiter,
|
||||
h1: spliceSettings?.h1,
|
||||
h2: spliceSettings?.h2,
|
||||
h3: spliceSettings?.h3,
|
||||
h4: spliceSettings?.h4,
|
||||
h5: spliceSettings?.h5,
|
||||
h6: spliceSettings?.h6,
|
||||
});
|
||||
} else {
|
||||
counter = new OrderedCounter({
|
||||
maxRecLevel,
|
||||
ignoreTopLevel,
|
||||
allowZeroLevel: orderedAllowZeroLevel,
|
||||
styleType: orderedStyleType,
|
||||
delimiter: orderedDelimiter,
|
||||
trailingDelimiter: orderedTrailingDelimiter,
|
||||
customTrailingDelimiter: orderedCustomTrailingDelimiter,
|
||||
leadingDelimiter: orderedLeadingDelimiter,
|
||||
customLeadingDelimiter: orderedCustomLeadingDelimiter,
|
||||
customIdents: getOrderedCustomIdents(orderedCustomIdents),
|
||||
specifiedString: orderedSpecifiedString,
|
||||
});
|
||||
}
|
||||
}
|
||||
const counter = createCounterFromSettings(settings, doc);
|
||||
|
||||
const heading = new Heading();
|
||||
for (let lineIndex = 1; lineIndex <= doc.lines; lineIndex++) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
debounce,
|
||||
} from "obsidian";
|
||||
import { EditorView, ViewPlugin } from "@codemirror/view";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { i18n } from "../locales";
|
||||
import type { HeadingPluginSettings, HeadingPluginData } from "../common/data";
|
||||
import {
|
||||
|
|
@ -23,6 +24,7 @@ import {
|
|||
editorModeField,
|
||||
updateEditorMode,
|
||||
} from "./editor";
|
||||
import { createHeadingGutterExtension } from "./editor-gutter";
|
||||
import { ViewChildComponent } from "./child";
|
||||
import { ReadingChild } from "./reading-child";
|
||||
import {
|
||||
|
|
@ -61,6 +63,7 @@ export class HeadingPlugin extends Plugin {
|
|||
i18n = i18n;
|
||||
|
||||
private revokes: (() => void)[] = [];
|
||||
private editorExtensions: Extension[] = [];
|
||||
|
||||
private readingComponents: ReadingChild[] = [];
|
||||
|
||||
|
|
@ -165,11 +168,8 @@ export class HeadingPlugin extends Plugin {
|
|||
await this.loadSettings();
|
||||
|
||||
// Register editor extension
|
||||
this.registerEditorExtension([
|
||||
headingDecorationsField,
|
||||
editorModeField,
|
||||
this.craeteHeadingViewPlugin(this.getPluginData.bind(this)),
|
||||
]);
|
||||
this.editorExtensions = this.buildEditorExtensions();
|
||||
this.registerEditorExtension(this.editorExtensions);
|
||||
|
||||
// Register markdown post processor
|
||||
this.registerMarkdownPostProcessor((element, context) => {
|
||||
|
|
@ -348,6 +348,8 @@ export class HeadingPlugin extends Plugin {
|
|||
} else {
|
||||
this.unloadFileExplorerComponents();
|
||||
}
|
||||
} else if (path === "useGutter" || path === "gutterPosition") {
|
||||
this.swapEditorExtensions();
|
||||
} else if (
|
||||
path === "enabledOutlineSettings" ||
|
||||
path.startsWith("outlineSettings")
|
||||
|
|
@ -434,7 +436,30 @@ export class HeadingPlugin extends Plugin {
|
|||
this.fileExplorerIdSet.clear();
|
||||
}
|
||||
|
||||
private craeteHeadingViewPlugin(
|
||||
private buildEditorExtensions(): Extension[] {
|
||||
const getPluginData = this.getPluginData.bind(this);
|
||||
if (this.settings.useGutter) {
|
||||
const showBefore = this.settings.gutterPosition === "before-line-numbers";
|
||||
return [
|
||||
editorModeField,
|
||||
...createHeadingGutterExtension(getPluginData, showBefore),
|
||||
];
|
||||
}
|
||||
return [
|
||||
headingDecorationsField,
|
||||
editorModeField,
|
||||
this.createInlineViewPlugin(getPluginData),
|
||||
];
|
||||
}
|
||||
|
||||
private swapEditorExtensions(): void {
|
||||
const newExts = this.buildEditorExtensions();
|
||||
this.editorExtensions.length = 0;
|
||||
this.editorExtensions.push(...newExts);
|
||||
this.app.workspace.updateOptions();
|
||||
}
|
||||
|
||||
private createInlineViewPlugin(
|
||||
getPluginData: () => Promise<HeadingPluginData>
|
||||
) {
|
||||
return ViewPlugin.fromClass(
|
||||
|
|
|
|||
|
|
@ -74,6 +74,49 @@ export class HeadingSettingTab extends PluginSettingTab {
|
|||
});
|
||||
});
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(i18n.t("setting.editorDisplay"))
|
||||
.setHeading();
|
||||
|
||||
//* useGutter
|
||||
const gutterPositionManager = new SettingDisplayManager();
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(i18n.t("setting.useGutter"))
|
||||
.setDesc(i18n.t("setting.useGutterDesc"))
|
||||
.addToggle((toggle) =>
|
||||
toggle.setValue(settings.useGutter).onChange((value) => {
|
||||
settings.useGutter = value;
|
||||
value ? gutterPositionManager.show() : gutterPositionManager.hide();
|
||||
this.plugin.saveSettings();
|
||||
})
|
||||
);
|
||||
|
||||
//* gutterPosition
|
||||
gutterPositionManager.add(
|
||||
new Setting(containerEl)
|
||||
.setName(i18n.t("setting.gutterPosition"))
|
||||
.setDesc(i18n.t("setting.gutterPositionDesc"))
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOptions({
|
||||
"before-line-numbers": i18n.t("setting.beforeLineNumbers"),
|
||||
"after-line-numbers": i18n.t("setting.afterLineNumbers"),
|
||||
})
|
||||
.setValue(settings.gutterPosition)
|
||||
.onChange((value) => {
|
||||
settings.gutterPosition = this.isGutterPosition(value)
|
||||
? value
|
||||
: "before-line-numbers";
|
||||
this.plugin.saveSettings();
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
if (!settings.useGutter) {
|
||||
gutterPositionManager.hide();
|
||||
}
|
||||
|
||||
new Setting(containerEl).setName(i18n.t("setting.reading")).setHeading();
|
||||
|
||||
//* enabledInReading
|
||||
|
|
@ -448,6 +491,10 @@ export class HeadingSettingTab extends PluginSettingTab {
|
|||
return ["partial", "full"].includes(value);
|
||||
}
|
||||
|
||||
private isGutterPosition(value: string): value is GutterPosition {
|
||||
return ["before-line-numbers", "after-line-numbers"].includes(value);
|
||||
}
|
||||
|
||||
private manageHeadingDecoratorSettings(
|
||||
settingsType: PluginDecoratorSettingsType
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -154,6 +154,13 @@
|
|||
"fileRegexBlocklistIndex": "Note name regex blocklist {index}",
|
||||
"fileRegexBlocklistPlaceholder": "e.g., /^daily.*/i",
|
||||
"fileRegexBlocklistAddTip": "Add a new note name regex blocklist",
|
||||
"editorDisplay": "Editor display",
|
||||
"useGutter": "Use gutter for editor decorations",
|
||||
"useGutterDesc": "Render heading decorations in a gutter column instead of inline with the text. Applies to both live preview and source mode.",
|
||||
"gutterPosition": "Gutter position",
|
||||
"gutterPositionDesc": "Whether the gutter column appears before or after line numbers.",
|
||||
"beforeLineNumbers": "Before line numbers",
|
||||
"afterLineNumbers": "After line numbers",
|
||||
"logic": "Logic",
|
||||
"h1": "H1",
|
||||
"h2": "H2",
|
||||
|
|
|
|||
|
|
@ -154,6 +154,13 @@
|
|||
"fileRegexBlocklistIndex": "筆記名稱正則表示式阻止列表 {index}",
|
||||
"fileRegexBlocklistPlaceholder": "例如:/^daily.*/i",
|
||||
"fileRegexBlocklistAddTip": "新增新的筆記名稱正則表示式到阻止列表",
|
||||
"editorDisplay": "編輯器顯示",
|
||||
"useGutter": "使用邊欄顯示編輯器裝飾",
|
||||
"useGutterDesc": "在邊欄列中顯示標題裝飾,而不是內嵌在文字中。適用於實際預覽和原始碼模式。",
|
||||
"gutterPosition": "邊欄位置",
|
||||
"gutterPositionDesc": "邊欄列顯示在行號之前還是之後。",
|
||||
"beforeLineNumbers": "在行號之前",
|
||||
"afterLineNumbers": "在行號之後",
|
||||
"logic": "邏輯控制",
|
||||
"h1": "H1 級別",
|
||||
"h2": "H2 級別",
|
||||
|
|
|
|||
|
|
@ -154,6 +154,13 @@
|
|||
"fileRegexBlocklistIndex": "笔记名称正则表达式阻止列表 {index}",
|
||||
"fileRegexBlocklistPlaceholder": "例如:/^daily.*/i",
|
||||
"fileRegexBlocklistAddTip": "添加新的笔记名称正则表达式到阻止列表",
|
||||
"editorDisplay": "编辑器显示",
|
||||
"useGutter": "使用边栏显示编辑器装饰",
|
||||
"useGutterDesc": "在边栏列中显示标题装饰,而不是内联在文本中。适用于实时阅览和源码模式。",
|
||||
"gutterPosition": "边栏位置",
|
||||
"gutterPositionDesc": "边栏列显示在行号之前还是之后。",
|
||||
"beforeLineNumbers": "在行号之前",
|
||||
"afterLineNumbers": "在行号之后",
|
||||
"logic": "逻辑控制",
|
||||
"h1": "H1 级别",
|
||||
"h2": "H2 级别",
|
||||
|
|
|
|||
53
styles.css
53
styles.css
|
|
@ -442,4 +442,55 @@ body {
|
|||
.file-explorer-custom-heading-decorator.after-heading-decorator[data-decorator-opacity="100%"]::after {
|
||||
opacity: 100%;
|
||||
}
|
||||
/* file-explorer start */
|
||||
/* file-explorer end */
|
||||
|
||||
/* gutter start */
|
||||
.heading-decorator-gutter {
|
||||
font-size: var(--font-smallest);
|
||||
letter-spacing: 0.015em;
|
||||
text-align: end;
|
||||
min-width: 3ch;
|
||||
}
|
||||
|
||||
.heading-decorator-gutter-marker {
|
||||
color: var(--text-faint);
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="10%"] {
|
||||
opacity: 10%;
|
||||
}
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="20%"] {
|
||||
opacity: 20%;
|
||||
}
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="30%"] {
|
||||
opacity: 30%;
|
||||
}
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="40%"] {
|
||||
opacity: 40%;
|
||||
}
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="50%"] {
|
||||
opacity: 50%;
|
||||
}
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="60%"] {
|
||||
opacity: 60%;
|
||||
}
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="70%"] {
|
||||
opacity: 70%;
|
||||
}
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="80%"] {
|
||||
opacity: 80%;
|
||||
}
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="90%"] {
|
||||
opacity: 90%;
|
||||
}
|
||||
.heading-decorator-gutter-marker[data-decorator-opacity="100%"] {
|
||||
opacity: 100%;
|
||||
}
|
||||
|
||||
.table-cell-wrapper .heading-decorator-gutter {
|
||||
display: none;
|
||||
}
|
||||
/* gutter end */
|
||||
|
|
|
|||
1
types.d.ts
vendored
1
types.d.ts
vendored
|
|
@ -13,6 +13,7 @@ type HeadingTuple = TupleOf<string, 6>;
|
|||
type DecoratorMode = "orderd" | "independent" | "splice" | "unordered";
|
||||
type OpacityOptions = 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100;
|
||||
type PostionOptions = "before" | "before-inside" | "after" | "after-inside";
|
||||
type GutterPosition = "before-line-numbers" | "after-line-numbers";
|
||||
type RenderPolicy = "partial" | "full";
|
||||
|
||||
type PluginDecoratorSettingsType =
|
||||
|
|
|
|||
Loading…
Reference in a new issue