mirror of
https://github.com/jesse-r-s-hines/obsidian-open-tab-settings.git
synced 2026-07-22 05:43:41 +00:00
Add VSCode style preview tabs
This commit is contained in:
parent
b156ddac6f
commit
ec54faa8f9
4 changed files with 106 additions and 21 deletions
94
src/main.ts
94
src/main.ts
|
|
@ -1,6 +1,6 @@
|
|||
import {
|
||||
Plugin, Workspace, WorkspaceLeaf, WorkspaceRoot, WorkspaceFloating, View, TFile, PaneType, WorkspaceTabs,
|
||||
WorkspaceItem, Platform, Keymap, Notice, App,
|
||||
WorkspaceItem, Platform, Keymap, Notice, App, MarkdownView,
|
||||
} from 'obsidian';
|
||||
import * as monkeyAround from 'monkey-around';
|
||||
import {
|
||||
|
|
@ -59,6 +59,7 @@ const OVERRIDES = {
|
|||
same: buildOverride(false, {openInNewTab: false}),
|
||||
allow_duplicate: buildOverride(false, {deduplicateTabs: false}),
|
||||
opposite: buildOverride("tab", {newTabTabGroupPlacement: "opposite"}),
|
||||
no_preview: buildOverride("tab", {previewTabs: false}),
|
||||
}
|
||||
|
||||
export default class OpenTabSettingsPlugin extends Plugin {
|
||||
|
|
@ -150,6 +151,19 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.registerEvent(this.app.workspace.on("editor-change", (editor, info) => {
|
||||
if (info instanceof MarkdownView) {
|
||||
this.setLeafIsPreview(info.leaf, false);
|
||||
}
|
||||
}))
|
||||
|
||||
this.register(() => {
|
||||
this.app.workspace.iterateAllLeaves(l => {
|
||||
this.setLeafIsPreview(l, false);
|
||||
delete l.openTabSettings;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
registerMonkeyPatches() {
|
||||
|
|
@ -178,8 +192,8 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
|
||||
// we set these to be used in openFile so we can tell when to deduplicate files.
|
||||
leaf.openTabSettings = {
|
||||
openMode, override,
|
||||
openedFrom: activeLeaf?.id,
|
||||
...leaf.openTabSettings,
|
||||
openInfo: { openMode, override, openedFrom: activeLeaf?.id },
|
||||
}
|
||||
|
||||
return leaf;
|
||||
|
|
@ -213,9 +227,9 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
|
||||
// these values are only valid immediately after creating a leaf. We clear them after openFile,
|
||||
// and also clear them here if the leaf somehow gets populated without openFile
|
||||
if (!isEmptyLeaf(this)) delete this.openTabSettings;
|
||||
if (!isEmptyLeaf(this)) delete this.openTabSettings?.openInfo;
|
||||
|
||||
const {openMode, override, openedFrom} = this.openTabSettings ?? {};
|
||||
const {openMode, override, openedFrom} = this.openTabSettings?.openInfo ?? {};
|
||||
const settings = {...plugin.settings, ...override};
|
||||
|
||||
let matches = plugin.findMatchingLeaves(file);
|
||||
|
|
@ -239,7 +253,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
);
|
||||
|
||||
let match: WorkspaceLeaf|undefined;
|
||||
if (matches.includes(this)) match = this;
|
||||
if (matches.includes(this)) match = this; // eslint-disable-line @typescript-eslint/no-this-alias
|
||||
// if the link opened was an internal link, always deduplicate to undo open in new tab.
|
||||
if (!match && isInternalLink && !isSpecialOpen) {
|
||||
match = matches.find(l => l.id == openedFrom)!;
|
||||
|
|
@ -283,7 +297,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
delete this.openTabSettings;
|
||||
delete this.openTabSettings?.openInfo;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -321,8 +335,14 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
async updateSettings(newSettings: Partial<OpenTabSettingsPluginSettings>) {
|
||||
const settings = {...this.settings, ...newSettings}
|
||||
|
||||
if (settings.previewTabs == true && !settings.openInNewTab) {
|
||||
if (newSettings.previewTabs) throw Error(`Invalid settings: ${JSON.stringify(newSettings)}`)
|
||||
settings.previewTabs = false;
|
||||
}
|
||||
|
||||
if (
|
||||
(settings.modClickBehavior == 'same' && !settings.openInNewTab) ||
|
||||
(settings.modClickBehavior == 'no_preview' && !settings.previewTabs) ||
|
||||
(settings.modClickBehavior == 'allow_duplicate' && !settings.deduplicateTabs)
|
||||
) {
|
||||
if (newSettings.modClickBehavior) throw Error(`Invalid settings: ${JSON.stringify(newSettings)}`)
|
||||
|
|
@ -330,6 +350,9 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
|
||||
Object.assign(this.settings, settings);
|
||||
if (!this.settings.previewTabs) {
|
||||
this.app.workspace.iterateAllLeaves(l => this.setLeafIsPreview(l, false));
|
||||
}
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
|
|
@ -365,6 +388,33 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
return [...tabGroups];
|
||||
}
|
||||
|
||||
private setLeafIsPreview(leaf: WorkspaceLeaf, isPreview: boolean) {
|
||||
if (leaf.openTabSettings?.isPreview === isPreview) return;
|
||||
|
||||
leaf.openTabSettings = {...leaf.openTabSettings, isPreview};
|
||||
leaf.tabHeaderEl.toggleClass("open-tab-settings-is-preview", isPreview);
|
||||
if (isPreview) {
|
||||
if (!leaf.openTabSettings.eventCleanup) {
|
||||
// I've confirmed that the events automatically get cleaned up when the leaf is closed. However we can't
|
||||
// use this.registerEvent as that prevents leaf garbage collection. So instead add a cleanup function to
|
||||
// the leaf. We'll call that on plugin disable, and after unpreview of a leaf
|
||||
const unPreview = () => { this.setLeafIsPreview(leaf, false); };
|
||||
leaf.on("pinned-change", unPreview);
|
||||
leaf.tabHeaderEl.addEventListener("dblclick", unPreview);
|
||||
leaf.openTabSettings.eventCleanup = () => {
|
||||
leaf.off('pinned-change', unPreview);
|
||||
leaf.tabHeaderEl.removeEventListener('dblclick', unPreview);
|
||||
}
|
||||
}
|
||||
// one preview tab per tab group (this shouldn't trigger under normal circumstances, but with empty tabs
|
||||
// there's a few edge cases where createNewLeaf might end up creating 2 preview tabs in a group)
|
||||
leaf.parent.children.filter(l => l !== leaf).forEach(l => this.setLeafIsPreview(l, false));
|
||||
} else if (leaf.openTabSettings.eventCleanup) {
|
||||
leaf.openTabSettings.eventCleanup();
|
||||
delete leaf.openTabSettings?.eventCleanup;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom variant of the internal workspace.createLeafInTabGroup function that follows our new tab placement logic.
|
||||
* @param focus Whether to focus the new tab. If undefined focus based on focusNewTab config
|
||||
|
|
@ -378,12 +428,6 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
if (!activeLeaf) throw new Error("No tab group found.");
|
||||
const activeTabGroup = activeLeaf.parent;
|
||||
const activeIndex = activeTabGroup.children.indexOf(activeLeaf);
|
||||
|
||||
// This is default Obsidian behavior, if active leaf is empty new tab replaces it instead of making a new one.
|
||||
if (isEmptyLeaf(activeLeaf) && activeLeaf.canNavigate()) {
|
||||
return activeLeaf;
|
||||
}
|
||||
|
||||
let group: TabGroup|undefined;
|
||||
let index: number|undefined;
|
||||
|
||||
|
|
@ -421,13 +465,26 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
let newLeaf: WorkspaceLeaf;
|
||||
// we re-use empty tabs more aggressively than default Obsidian. If the tab at the new location is empty, re-use
|
||||
// it instead of creating a new one.
|
||||
let newLeaf: WorkspaceLeaf|undefined;
|
||||
|
||||
// This is default Obsidian behavior, if active leaf is empty new tab replaces it instead of making a new one.
|
||||
if (isEmptyLeaf(activeLeaf) && activeLeaf.canNavigate()) {
|
||||
newLeaf = activeLeaf;
|
||||
}
|
||||
|
||||
const leafToDisplace = group.children[Math.min(index, group.children.length - 1)];
|
||||
if (isEmptyLeaf(leafToDisplace) && leafToDisplace.canNavigate()) {
|
||||
if (!newLeaf && isEmptyLeaf(leafToDisplace) && leafToDisplace.canNavigate()) {
|
||||
// we re-use empty tabs more aggressively than default Obsidian. If the tab at the new location is empty,
|
||||
// re-use it instead of creating a new one.
|
||||
newLeaf = leafToDisplace;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!newLeaf && settings.previewTabs) {
|
||||
// ignore index and use preview tab in group if there is one
|
||||
newLeaf = group.children.find(l => l.openTabSettings?.isPreview);
|
||||
}
|
||||
|
||||
if (!newLeaf) {
|
||||
newLeaf = new (WorkspaceLeaf as new (app: App) => WorkspaceLeaf)(this.app);
|
||||
const currentTab = group.currentTab;
|
||||
// If new tab is inserted before the currently tab in a group, and we aren't setting the new tab active, we
|
||||
|
|
@ -438,6 +495,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
this.setLeafIsPreview(newLeaf, settings.previewTabs);
|
||||
if (focus) {
|
||||
workspace.setActiveLeaf(newLeaf);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,10 +20,12 @@ export const MOD_CLICK_BEHAVIOR = {
|
|||
"same": "In same tab",
|
||||
"allow_duplicate": "In duplicate tab",
|
||||
"opposite": "In opposite tab group",
|
||||
"no_preview": "Open persistent (non-preview) tab",
|
||||
}
|
||||
|
||||
export interface OpenTabSettingsPluginSettings {
|
||||
openInNewTab: boolean,
|
||||
previewTabs: boolean,
|
||||
deduplicateTabs: boolean,
|
||||
deduplicateAcrossTabGroups: boolean,
|
||||
newTabPlacement: keyof typeof NEW_TAB_PLACEMENTS,
|
||||
|
|
@ -33,6 +35,7 @@ export interface OpenTabSettingsPluginSettings {
|
|||
|
||||
export const DEFAULT_SETTINGS: OpenTabSettingsPluginSettings = {
|
||||
openInNewTab: true,
|
||||
previewTabs: false,
|
||||
deduplicateTabs: true,
|
||||
deduplicateAcrossTabGroups: true,
|
||||
newTabPlacement: "after-active",
|
||||
|
|
@ -77,6 +80,19 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
|
|||
})
|
||||
);
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('Preview tabs')
|
||||
.setDesc('VS Code style preview tabs. Initially open tabs as "preview" until interacted with. Preview tabs will be replaced instead of opening in new tab.')
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.previewTabs)
|
||||
.onChange(async (value) => {
|
||||
await this.plugin.updateSettings({previewTabs: value});
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.setDisabled(!this.plugin.settings.openInNewTab);
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('Prevent duplicate tabs')
|
||||
.setDesc('If a tab is already open, switch to it instead of re-opening it.')
|
||||
|
|
@ -162,6 +178,9 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
|
|||
dropdown.addOption("allow_duplicate", MOD_CLICK_BEHAVIOR['allow_duplicate'])
|
||||
}
|
||||
dropdown.addOption("opposite", MOD_CLICK_BEHAVIOR['opposite'])
|
||||
if (this.plugin.settings.previewTabs) {
|
||||
dropdown.addOption("no_preview", MOD_CLICK_BEHAVIOR['no_preview'])
|
||||
}
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.modClickBehavior)
|
||||
.onChange(async value => {
|
||||
|
|
|
|||
11
src/types.d.ts
vendored
11
src/types.d.ts
vendored
|
|
@ -4,9 +4,14 @@ import { OpenTabSettingsPluginSettings } from './settings';
|
|||
declare module "obsidian" {
|
||||
interface WorkspaceLeaf {
|
||||
openTabSettings?: {
|
||||
openMode: PaneType|false,
|
||||
override: Partial<OpenTabSettingsPluginSettings>,
|
||||
openedFrom?: string,
|
||||
openInfo?: {
|
||||
openMode: PaneType|false,
|
||||
override: Partial<OpenTabSettingsPluginSettings>,
|
||||
openedFrom?: string,
|
||||
},
|
||||
/** true when a leaf has just been opened, set to false the leaf is interacted with */
|
||||
isPreview?: boolean,
|
||||
eventCleanup?: () => void,
|
||||
},
|
||||
pinned: boolean,
|
||||
}
|
||||
|
|
|
|||
3
styles.css
Normal file
3
styles.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.open-tab-settings-is-preview {
|
||||
font-style: italic;
|
||||
}
|
||||
Loading…
Reference in a new issue