mirror of
https://github.com/jesse-r-s-hines/obsidian-open-tab-settings.git
synced 2026-07-22 05:43:41 +00:00
feat: add max open tab limit with time-based eviction
This commit is contained in:
parent
ce42eb7003
commit
b5a9f09f77
3 changed files with 142 additions and 0 deletions
44
README.md
44
README.md
|
|
@ -1,8 +1,10 @@
|
|||
<<<<<<< HEAD
|
||||
# Open Tab Settings [](https://github.com/jesse-r-s-hines/obsidian-open-tab-settings/actions/workflows/test.yaml)
|
||||
|
||||
This plugin adds settings to customize how Obsidian opens tabs and navigates between files, including options to:
|
||||
- open in new tab by default
|
||||
- switch to existing tab instead of opening a duplicate file
|
||||
- set a max open tab limit per tab group and auto-close oldest tabs when exceeded
|
||||
- customize new tab placement and order
|
||||
- open new tabs in the opposite pane when using a split workspace
|
||||
|
||||
|
|
@ -37,3 +39,45 @@ Run them with:
|
|||
npm run test
|
||||
```
|
||||
This will automatically download the latest Obsidian version and test the plugin against it.
|
||||
=======
|
||||
# Open Tab Settings [](https://github.com/jesse-r-s-hines/obsidian-open-tab-settings/actions/workflows/test.yaml)
|
||||
|
||||
This plugin adds settings to customize how Obsidian opens tabs and navigates between files, including options to:
|
||||
- open in new tab by default
|
||||
- switch to existing tab instead of opening a duplicate file
|
||||
- set a max open tab limit per tab group and auto-close oldest tabs when exceeded
|
||||
- customize new tab placement and order
|
||||
- open new tabs in the opposite pane when using a split workspace
|
||||
|
||||
Using these settings can enable a more familiar workflow for those used to working in editors like VSCode. With the "Always open in new tab" toggled, Obsidian will always open files in a new tab, whether they were opened via links, the quick switcher, file explorer, etc. Never accidentally lose your tabs again!
|
||||
|
||||
## Features
|
||||

|
||||
|
||||
The primary settings can also be toggled via commands.
|
||||
|
||||
## Comparison with similar plugins
|
||||
There are several plugins that attempt to solve this problem with different pros and cons. However, most other options either only work in specific menus or have a noticeable timer delay before opening new tabs. "Open Tab Settings" works by patching some of Obsidian's internal methods to achieve consistent and seamless new tab and de-duplication behavior throughout Obsidian. It is inspired by the [Opener](https://github.com/aidan-gibson/obsidian-opener) plugin which worked in a similar way, but is no longer maintained and broken on the latest Obsidian (though it has since been [forked](https://github.com/lukemt/obsidian-opener)). "Open Tab Settings" also adds a few improvements over Opener, including making non-file views such as the Graph View also open in new tabs and adding several customization options for new tab placement.
|
||||
|
||||
## Plugin compatibility
|
||||
Open Tab Settings should be compatible with most other plugins, if you encounter any plugin incompatibility problems please submit an issue.
|
||||
|
||||
Some known plugin interactions:
|
||||
- The [PDF++](https://github.com/RyotaUshio/obsidian-pdf-plus) plugin customizes the opening behavior of PDFs which can override Open Tab Setting's behavior. You can make the plugins play nicely together by setting the "How to open..." options to "new tab" in the PDF++ settings.
|
||||
|
||||
|
||||
## Contributing
|
||||
You can build the plugin with:
|
||||
```shell
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
This plugin has end-to-end tests using [wdio-obsidian-service](https://github.com/jesse-r-s-hines/wdio-obsidian-service)
|
||||
and [WebdriverIO](https://webdriver.io/).
|
||||
Run them with:
|
||||
```shell
|
||||
npm run test
|
||||
```
|
||||
This will automatically download the latest Obsidian version and test the plugin against it.
|
||||
>>>>>>> 0daf4fd38171ffe202122fedbfb1ba6b8f0a865a
|
||||
|
|
|
|||
77
src/main.ts
77
src/main.ts
|
|
@ -63,6 +63,8 @@ const OVERRIDES = {
|
|||
|
||||
export default class OpenTabSettingsPlugin extends Plugin {
|
||||
settings: OpenTabSettingsPluginSettings = {...DEFAULT_SETTINGS};
|
||||
private leafOpenOrder: Map<string, number> = new Map();
|
||||
private nextOpenOrder = 1;
|
||||
|
||||
async onload() {
|
||||
await this.loadSettings();
|
||||
|
|
@ -208,6 +210,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
// openFile doesn't return anything, but just in case that changes.
|
||||
let result: any;
|
||||
let match: WorkspaceLeaf|undefined;
|
||||
let openedLeaf: WorkspaceLeaf|undefined;
|
||||
|
||||
// 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
|
||||
|
|
@ -253,6 +256,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
// https://github.com/jesse-r-s-hines/obsidian-open-tab-settings/issues/25
|
||||
// https://github.com/mgmeyers/obsidian-kanban/issues/1102
|
||||
plugin.app.workspace.setActiveLeaf(matches[0]);
|
||||
openedLeaf = matches[0];
|
||||
result = undefined;
|
||||
} else {
|
||||
const activeLeaf = plugin.app.workspace.getActiveViewOfType(View)?.leaf;
|
||||
|
|
@ -260,9 +264,11 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
...openState,
|
||||
active: !!openState?.active || activeLeaf == this,
|
||||
}, ...args);
|
||||
openedLeaf = matches[0];
|
||||
}
|
||||
} else { // use default behavior
|
||||
result = await oldMethod.call(this, file, openState, ...args);
|
||||
openedLeaf = this;
|
||||
}
|
||||
|
||||
// If the leaf is still empty, close it. This can happen if the file was de-duplicated while
|
||||
|
|
@ -274,11 +280,17 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
.filter(l => l !== this)
|
||||
.reduce((max, l) => l.activeTime > max.activeTime ? l : max);
|
||||
this.detach();
|
||||
plugin.leafOpenOrder.delete(this.id);
|
||||
if (wasCurrentTab) {
|
||||
tabGroup.selectTabIndex(tabGroup.children.findIndex(c => c === lastActiveTab));
|
||||
}
|
||||
}
|
||||
|
||||
if (openedLeaf && !isEmptyLeaf(openedLeaf)) {
|
||||
plugin.recordLeafOpenOrder(openedLeaf);
|
||||
plugin.enforceMaxOpenTabs(openedLeaf.parent, openedLeaf);
|
||||
}
|
||||
|
||||
delete this.openTabSettings;
|
||||
|
||||
return result;
|
||||
|
|
@ -305,6 +317,9 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
async loadSettings() {
|
||||
const dataFile = await this.loadData() ?? {};
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, dataFile);
|
||||
this.settings.maxOpenTabs = Number.isFinite(this.settings.maxOpenTabs) && this.settings.maxOpenTabs > 0
|
||||
? Math.floor(this.settings.maxOpenTabs)
|
||||
: 0;
|
||||
|
||||
if (Object.keys(dataFile).length == 0) {
|
||||
// when using this plugin, focusNewTab should default to false. Set it if this is the first time we've
|
||||
|
|
@ -315,10 +330,72 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
|
||||
async updateSettings(settings: Partial<OpenTabSettingsPluginSettings>) {
|
||||
if (settings.maxOpenTabs !== undefined) {
|
||||
settings.maxOpenTabs = Number.isFinite(settings.maxOpenTabs) && settings.maxOpenTabs > 0
|
||||
? Math.floor(settings.maxOpenTabs)
|
||||
: 0;
|
||||
}
|
||||
Object.assign(this.settings, settings);
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
private recordLeafOpenOrder(leaf: WorkspaceLeaf) {
|
||||
if (!this.leafOpenOrder.has(leaf.id)) {
|
||||
this.leafOpenOrder.set(leaf.id, this.nextOpenOrder++);
|
||||
}
|
||||
}
|
||||
|
||||
private ensureLeafOpenOrder(tabGroup: TabGroup) {
|
||||
const missingOrderLeaves = tabGroup.children
|
||||
.filter(l => !isEmptyLeaf(l) && !this.leafOpenOrder.has(l.id))
|
||||
.map((leaf, index) => ({
|
||||
leaf,
|
||||
index,
|
||||
activeTime: Number.isFinite(leaf.activeTime) ? leaf.activeTime : Number.POSITIVE_INFINITY,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.activeTime !== b.activeTime) return a.activeTime - b.activeTime;
|
||||
return a.index - b.index;
|
||||
});
|
||||
|
||||
for (const {leaf} of missingOrderLeaves) {
|
||||
this.leafOpenOrder.set(leaf.id, this.nextOpenOrder++);
|
||||
}
|
||||
}
|
||||
|
||||
private enforceMaxOpenTabs(tabGroup: TabGroup, currentLeaf?: WorkspaceLeaf) {
|
||||
const maxOpenTabs = this.settings.maxOpenTabs;
|
||||
if (maxOpenTabs <= 0) return;
|
||||
|
||||
this.ensureLeafOpenOrder(tabGroup);
|
||||
|
||||
const getOpenLeaves = () => tabGroup.children.filter(l => !isEmptyLeaf(l));
|
||||
let openLeaves = getOpenLeaves();
|
||||
while (openLeaves.length > maxOpenTabs) {
|
||||
const candidates = openLeaves.filter(l => l !== currentLeaf);
|
||||
const closePool = candidates.length > 0 ? candidates : openLeaves;
|
||||
|
||||
const leavesWithOrder = closePool
|
||||
.map(leaf => ({leaf, order: this.leafOpenOrder.get(leaf.id)}))
|
||||
.filter((value): value is {leaf: WorkspaceLeaf, order: number} => value.order !== undefined);
|
||||
|
||||
const leafToClose = leavesWithOrder.length > 0
|
||||
? leavesWithOrder.reduce((min, cur) => cur.order < min.order ? cur : min).leaf
|
||||
: closePool[0];
|
||||
|
||||
const closeIndex = tabGroup.children.indexOf(leafToClose);
|
||||
const wasCurrentTab = tabGroup.children[tabGroup.currentTab] === leafToClose;
|
||||
leafToClose.detach();
|
||||
this.leafOpenOrder.delete(leafToClose.id);
|
||||
|
||||
if (wasCurrentTab && tabGroup.children.length > 0) {
|
||||
tabGroup.selectTabIndex(Math.min(closeIndex, tabGroup.children.length - 1));
|
||||
}
|
||||
|
||||
openLeaves = getOpenLeaves();
|
||||
}
|
||||
}
|
||||
|
||||
private findMatchingLeaves(file: TFile) {
|
||||
const matches: WorkspaceLeaf[] = [];
|
||||
this.app.workspace.iterateAllLeaves(leaf => {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export interface OpenTabSettingsPluginSettings {
|
|||
openInNewTab: boolean,
|
||||
deduplicateTabs: boolean,
|
||||
deduplicateAcrossTabGroups: boolean,
|
||||
maxOpenTabs: number,
|
||||
newTabPlacement: keyof typeof NEW_TAB_PLACEMENTS,
|
||||
newTabTabGroupPlacement: "same"|"opposite"|"first"|"last",
|
||||
modClickBehavior: keyof typeof MOD_CLICK_BEHAVIOR,
|
||||
|
|
@ -35,6 +36,7 @@ export const DEFAULT_SETTINGS: OpenTabSettingsPluginSettings = {
|
|||
openInNewTab: true,
|
||||
deduplicateTabs: true,
|
||||
deduplicateAcrossTabGroups: true,
|
||||
maxOpenTabs: 0,
|
||||
newTabPlacement: "after-active",
|
||||
newTabTabGroupPlacement: "same",
|
||||
modClickBehavior: "tab",
|
||||
|
|
@ -96,6 +98,25 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
|
|||
.settingEl
|
||||
.setCssStyles({opacity: this.plugin.settings.deduplicateTabs ? "" : "50%"});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('Maximum open tabs')
|
||||
.setDesc(
|
||||
'Limit the number of tabs per tab group. 0 disables the limit. ' +
|
||||
'When the limit is exceeded, the oldest opened tab is closed. ' +
|
||||
'If open order cannot be determined, the leftmost tab is closed.'
|
||||
)
|
||||
.addText(text =>
|
||||
text
|
||||
.setPlaceholder('0')
|
||||
.setValue(String(this.plugin.settings.maxOpenTabs))
|
||||
.onChange(async (value) => {
|
||||
const parsed = Number.parseInt(value.trim(), 10);
|
||||
await this.plugin.updateSettings({
|
||||
maxOpenTabs: Number.isFinite(parsed) && parsed > 0 ? parsed : 0,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('Focus explicit new tabs')
|
||||
.setDesc(
|
||||
|
|
|
|||
Loading…
Reference in a new issue