mirror of
https://github.com/jesse-r-s-hines/obsidian-open-tab-settings.git
synced 2026-07-22 12:20:25 +00:00
Compare commits
22 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55db9ae77a | ||
|
|
558beaa9ae | ||
|
|
ca345a3333 | ||
|
|
55d1f2d08a | ||
|
|
d105532b6a | ||
|
|
abbb710f93 | ||
|
|
83e8e262ba | ||
|
|
207eeb0c23 | ||
|
|
ace8e3074b | ||
|
|
ec54faa8f9 | ||
|
|
b156ddac6f | ||
|
|
fb765508c8 | ||
|
|
eca003209f | ||
|
|
9bee6ad3f6 | ||
|
|
ba44f52414 | ||
|
|
9da13da847 | ||
|
|
165be33e40 | ||
|
|
d4edcbd545 | ||
|
|
795169f942 | ||
|
|
c80a127cc6 | ||
|
|
5d625db043 | ||
|
|
f3379e9931 |
27 changed files with 4762 additions and 4432 deletions
|
|
@ -22,18 +22,17 @@ Open Tab Settings should be compatible with most other plugins, if you encounter
|
|||
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:
|
||||
Clone the repository and 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:
|
||||
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.
|
||||
|
||||
If you'd like to help with translating, add just a new locale file under `src/locales`.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import { builtinModules } from "node:module";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
|
|
@ -17,6 +19,32 @@ const context = await esbuild.context({
|
|||
},
|
||||
entryPoints: ["src/main.ts"],
|
||||
bundle: true,
|
||||
plugins: [{
|
||||
name: "import-glob",
|
||||
setup: (build) => {
|
||||
build.onResolve({ filter: /^glob:.*$/ }, async (args) => {
|
||||
return {
|
||||
path: args.path.replace(/^glob:/, ''),
|
||||
namespace: 'import-glob',
|
||||
pluginData: { resolveDir: args.resolveDir },
|
||||
};
|
||||
});
|
||||
build.onLoad({ filter: /.*/, namespace: 'import-glob' }, async (args) => {
|
||||
const files = fs.globSync(args.path, {cwd: args.pluginData.resolveDir})
|
||||
.map((f) => f.split(path.sep).join('/'))
|
||||
.sort();
|
||||
return {
|
||||
contents: [
|
||||
...files.map((module, i) => `import m${i} from './${module}'`),
|
||||
"export default {",
|
||||
...files.map((f, i) => `${JSON.stringify(path.basename(f, path.extname(f)))}: m${i},`),
|
||||
"}",
|
||||
].join("\n"),
|
||||
resolveDir: args.pluginData.resolveDir,
|
||||
};
|
||||
});
|
||||
},
|
||||
}],
|
||||
external: [
|
||||
"obsidian",
|
||||
"electron",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "open-tab-settings",
|
||||
"name": "Open Tab Settings",
|
||||
"version": "2.0.3",
|
||||
"minAppVersion": "1.8.3",
|
||||
"version": "2.2.0",
|
||||
"minAppVersion": "1.8.7",
|
||||
"description": "Adds options to customize how tabs are opened, including open in new tab by default, preventing duplicate tabs, and more.",
|
||||
"author": "jesse-r-s-hines",
|
||||
"authorUrl": "https://github.com/jesse-r-s-hines",
|
||||
|
|
|
|||
8214
package-lock.json
generated
8214
package-lock.json
generated
File diff suppressed because it is too large
Load diff
37
package.json
37
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "obsidian-open-tab-settings",
|
||||
"version": "2.0.3",
|
||||
"version": "2.2.0",
|
||||
"description": "Add settings to open in new tab by default and prevent duplicate tabs.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
|
@ -21,32 +21,33 @@
|
|||
"@eslint/js": "^10.0.1",
|
||||
"@types/chai": "^5.2.3",
|
||||
"@types/mocha": "^10.0.10",
|
||||
"@types/node": "^25.7.0",
|
||||
"@wdio/appium-service": "^9.27.1",
|
||||
"@wdio/cli": "^9.27.1",
|
||||
"@wdio/local-runner": "^9.27.1",
|
||||
"@wdio/mocha-framework": "^9.27.1",
|
||||
"@wdio/spec-reporter": "^9.27.1",
|
||||
"appium": "^3.4.2",
|
||||
"appium-uiautomator2-driver": "^7.2.3",
|
||||
"@types/node": "^25.9.3",
|
||||
"@wdio/appium-service": "^9.28.0",
|
||||
"@wdio/cli": "^9.28.0",
|
||||
"@wdio/local-runner": "^9.28.0",
|
||||
"@wdio/mocha-framework": "^9.28.0",
|
||||
"@wdio/spec-reporter": "^9.28.0",
|
||||
"appium": "^3.5.0",
|
||||
"appium-uiautomator2-driver": "^7.6.1",
|
||||
"chai": "^6.2.2",
|
||||
"deepmerge-ts": "^7.1.5",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "^10.3.0",
|
||||
"eslint": "^10.4.1",
|
||||
"eslint-plugin-obsidianmd": "^0.3.0",
|
||||
"mocha": "^11.7.5",
|
||||
"mocha": "^11.7.6",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"obsidian": "latest",
|
||||
"obsidian-launcher": "^3.0.2",
|
||||
"obsidian-typings": "^5.21.0",
|
||||
"obsidian": "1.12.3",
|
||||
"obsidian-launcher": "^3.1.1",
|
||||
"obsidian-typings": "^5.24.0",
|
||||
"tslib": "^2.8.1",
|
||||
"tsx": "^4.21.0",
|
||||
"tsx": "^4.22.4",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.59.3",
|
||||
"wdio-obsidian-reporter": "^3.0.2",
|
||||
"wdio-obsidian-service": "^3.0.2"
|
||||
"typescript-eslint": "^8.61.0",
|
||||
"wdio-obsidian-reporter": "^3.1.1",
|
||||
"wdio-obsidian-service": "^3.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18next": "^24.2.1",
|
||||
"monkey-around": "^3.0.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 132 KiB |
5
src/glob.d.ts
vendored
Normal file
5
src/glob.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
|
||||
declare module 'glob:*' {
|
||||
const modules: Record<string, any>;
|
||||
export default modules;
|
||||
}
|
||||
12
src/i18n.ts
Normal file
12
src/i18n.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import i18next from 'i18next';
|
||||
import { getLanguage } from 'obsidian';
|
||||
import locales from 'glob:./locales/*.json';
|
||||
|
||||
export async function initializeI18n() {
|
||||
return await i18next.init({
|
||||
lng: getLanguage(),
|
||||
fallbackLng: 'en',
|
||||
resources: Object.fromEntries(Object.entries(locales).map(([k, v]) => [k, {translation: v}])),
|
||||
interpolation: { escapeValue: false },
|
||||
});
|
||||
};
|
||||
73
src/locales/en.json
Normal file
73
src/locales/en.json
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"commands": {
|
||||
"toggle": "Toggle {{name}}",
|
||||
"enable": "Enable {{name}}",
|
||||
"disable": "Disable {{name}}",
|
||||
"cycle": "Cycle {{name}}",
|
||||
"enabled": "ON",
|
||||
"disabled": "OFF"
|
||||
},
|
||||
"menu": {
|
||||
"openInSameTab": "Open in same tab",
|
||||
"openInDuplicateTab": "Open in duplicate tab",
|
||||
"openInOppositeTabGroup": "Open in opposite tab group"
|
||||
},
|
||||
"settings": {
|
||||
"deduplicateAcrossTabGroups": {
|
||||
"name": "Deduplicate across tab groups",
|
||||
"description": "Whether to switch to already open file even if its in a split pane or popout window"
|
||||
},
|
||||
"deduplicateTabs": {
|
||||
"name": "Prevent duplicate tabs",
|
||||
"description": "If a tab is already open, switch to it instead of re-opening it."
|
||||
},
|
||||
"disableOnDevice": {
|
||||
"name": "Disable on this device",
|
||||
"description": "When enabled, this plugin does nothing on the current device. This setting is per-device and is not synced. Restart Obsidian or toggle the plugin off and on to apply.",
|
||||
"notice": "Restart Obsidian for disable on this device to take effect"
|
||||
},
|
||||
"focusNewTabs": {
|
||||
"name": "Focus explicit new tabs",
|
||||
"description": "Immediately switch to new tabs opened via middle or ctrl click instead of opening them in the background. New tabs created by regular click will always focus regardless."
|
||||
},
|
||||
"modClickBehavior": {
|
||||
"name": "Mod click behavior",
|
||||
"description": "On ctrl/cmd/middle click open links...",
|
||||
"options": {
|
||||
"allow_duplicate": "In duplicate tab",
|
||||
"no_preview": "Open persistent (non-preview) tab",
|
||||
"opposite": "In opposite tab group",
|
||||
"same": "In same tab",
|
||||
"tab": "In new tab"
|
||||
}
|
||||
},
|
||||
"newTabPlacement": {
|
||||
"name": "New tab placement",
|
||||
"description": "Place new tabs...",
|
||||
"options": {
|
||||
"after-active": "After active tab",
|
||||
"after-pinned": "After pinned tabs",
|
||||
"beginning": "At beginning",
|
||||
"end": "At end"
|
||||
}
|
||||
},
|
||||
"newTabTabGroupPlacement": {
|
||||
"name": "New tab tab group placement",
|
||||
"description": "When the workspace is split, prefer to open new tabs...",
|
||||
"options": {
|
||||
"first": "In first tab group",
|
||||
"last": "In last tab group",
|
||||
"opposite": "In opposite tab group",
|
||||
"same": "In same tab group"
|
||||
}
|
||||
},
|
||||
"openInNewTab": {
|
||||
"name": "Always open in new tab",
|
||||
"description": "Open files in a new tab by default."
|
||||
},
|
||||
"previewTabs": {
|
||||
"name": "Preview tabs",
|
||||
"description": "VS Code style preview tabs. Initially open tabs as \"preview\" until interacted with. Preview tabs will be replaced instead of opening in new tab."
|
||||
}
|
||||
}
|
||||
}
|
||||
238
src/main.ts
238
src/main.ts
|
|
@ -1,12 +1,15 @@
|
|||
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 {
|
||||
OpenTabSettingsPluginSettingTab, OpenTabSettingsPluginSettings, DEFAULT_SETTINGS, NEW_TAB_TAB_GROUP_PLACEMENTS,
|
||||
isDisabledOnDevice,
|
||||
} from './settings';
|
||||
import { TabGroup } from './types';
|
||||
import { initializeI18n } from './i18n';
|
||||
import { t } from 'i18next';
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -14,7 +17,7 @@ import { TabGroup } from './types';
|
|||
* This is only needed if the view is not registered as the default view for a file extension.
|
||||
*/
|
||||
const PLUGIN_VIEW_TYPES: Record<string, string[]> = {
|
||||
"md": ["excalidraw", "kanban"],
|
||||
"md": ["excalidraw", "kanban", "smm"],
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -29,10 +32,6 @@ function isMainLeaf(leaf: WorkspaceLeaf) {
|
|||
return (root instanceof WorkspaceRoot || root instanceof WorkspaceFloating);
|
||||
}
|
||||
|
||||
function capitalize(s: string) {
|
||||
return s[0].toUpperCase() + s.slice(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a bit hacky, but to support easily changing our settings in Mod click or menu items we're sticking the
|
||||
* overrides onto the string passed to getLeaf.
|
||||
|
|
@ -58,96 +57,114 @@ 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 {
|
||||
settings: OpenTabSettingsPluginSettings = {...DEFAULT_SETTINGS};
|
||||
|
||||
async onload() {
|
||||
await initializeI18n();
|
||||
|
||||
await this.loadSettings();
|
||||
|
||||
this.addSettingTab(new OpenTabSettingsPluginSettingTab(this.app, this));
|
||||
|
||||
if (isDisabledOnDevice()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.registerMonkeyPatches();
|
||||
|
||||
this.registerEvent(
|
||||
this.app.workspace.on("file-menu", (menu, file, source, leaf) => {
|
||||
if (file instanceof TFile) {
|
||||
if (this.settings.openInNewTab) {
|
||||
menu.addItem((item) => {
|
||||
item.setSection("open");
|
||||
item.setIcon("file-minus")
|
||||
item.setTitle("Open in same tab");
|
||||
item.onClick(async () => {
|
||||
await this.app.workspace.getLeaf(OVERRIDES.same).openFile(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (this.settings.deduplicateTabs && this.findMatchingLeaves(file).length > 0) {
|
||||
menu.addItem((item) => {
|
||||
item.setSection("open");
|
||||
item.setIcon("files")
|
||||
item.setTitle("Open in duplicate tab");
|
||||
item.onClick(async () => {
|
||||
await this.app.workspace.getLeaf(OVERRIDES.allow_duplicate).openFile(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
const activeLeaf = this.app.workspace.getMostRecentLeaf();
|
||||
if (activeLeaf && this.getAllTabGroups(activeLeaf.getRoot()).length > 1) {
|
||||
menu.addItem((item) => {
|
||||
item.setSection("open");
|
||||
item.setIcon("lucide-split-square-horizontal")
|
||||
item.setTitle("Open in opposite tab group");
|
||||
item.onClick(async () => {
|
||||
await this.app.workspace.getLeaf(OVERRIDES.opposite).openFile(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
const commands = [
|
||||
["openInNewTab", "always open in new tab"],
|
||||
["deduplicateTabs", "prevent duplicate tabs"],
|
||||
["openInNewTab", t('settings.openInNewTab.name')],
|
||||
["deduplicateTabs", t('settings.deduplicateTabs.name')],
|
||||
] as const;
|
||||
for (const [setting, name] of commands) {
|
||||
const id = setting.replace(/[A-Z]/g, l => `-${l.toLowerCase()}`);
|
||||
|
||||
this.addCommand({
|
||||
id: `toggle-${id}`, name: `Toggle ${name}`,
|
||||
id: `toggle-${id}`, name: t('commands.toggle', { name }),
|
||||
callback: async () => {
|
||||
await this.updateSettings({[setting]: !this.settings[setting]});
|
||||
new Notice(`${capitalize(name)} ${this.settings[setting] ? 'ON' : 'OFF'}`, 2500);
|
||||
new Notice(`${name}: ` + t(`commands.${this.settings[setting] ? 'enabled' : 'disabled'}`), 2500);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: `enable-${id}`, name: `Enable ${name}`,
|
||||
id: `enable-${id}`, name: t('commands.enable', { name }),
|
||||
callback: async () => {
|
||||
await this.updateSettings({[setting]: true});
|
||||
new Notice(`${capitalize(name)} ${this.settings[setting] ? 'ON' : 'OFF'}`, 2500);
|
||||
new Notice(`${name}: ` + t(`commands.${this.settings[setting] ? 'enabled' : 'disabled'}`), 2500);
|
||||
},
|
||||
});
|
||||
this.addCommand({
|
||||
id: `disable-${id}`, name: `Disable ${name}`,
|
||||
id: `disable-${id}`, name: t('commands.disable', { name }),
|
||||
callback: async () => {
|
||||
await this.updateSettings({[setting]: false});
|
||||
new Notice(`${capitalize(name)} ${this.settings[setting] ? 'ON' : 'OFF'}`, 2500);
|
||||
new Notice(`${name}: ` + t(`commands.${this.settings[setting] ? 'enabled' : 'disabled'}`), 2500);
|
||||
},
|
||||
});
|
||||
}
|
||||
this.addCommand({
|
||||
id: `cycle-tab-group-placement`, name: `Cycle tab group placement`,
|
||||
id: `cycle-tab-group-placement`,
|
||||
name: t('commands.cycle', {name: t('settings.newTabTabGroupPlacement.name')}),
|
||||
callback: async () => {
|
||||
const values = Object.keys(NEW_TAB_TAB_GROUP_PLACEMENTS) as (keyof typeof NEW_TAB_TAB_GROUP_PLACEMENTS)[];
|
||||
const index = values.findIndex(v => v == this.settings.newTabTabGroupPlacement);
|
||||
const newValue = values[(index + 1) % values.length];
|
||||
await this.updateSettings({newTabTabGroupPlacement: newValue});
|
||||
new Notice(`Tab group placement: ${NEW_TAB_TAB_GROUP_PLACEMENTS[newValue]}`, 2500);
|
||||
new Notice(`${t('settings.newTabTabGroupPlacement.name')}: ${t(NEW_TAB_TAB_GROUP_PLACEMENTS[newValue])}`, 2500);
|
||||
},
|
||||
});
|
||||
|
||||
this.registerEvent(this.app.workspace.on("file-menu", (menu, file, source, leaf) => {
|
||||
if (file instanceof TFile) {
|
||||
if (this.settings.openInNewTab) {
|
||||
menu.addItem((item) => {
|
||||
item.setSection("open");
|
||||
item.setIcon("file-minus")
|
||||
item.setTitle(t('menu.openInSameTab'));
|
||||
item.onClick(async () => {
|
||||
await this.app.workspace.getLeaf(OVERRIDES.same).openFile(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (this.settings.deduplicateTabs && this.findMatchingLeaves(file).length > 0) {
|
||||
menu.addItem((item) => {
|
||||
item.setSection("open");
|
||||
item.setIcon("files")
|
||||
item.setTitle(t('menu.openInDuplicateTab'));
|
||||
item.onClick(async () => {
|
||||
await this.app.workspace.getLeaf(OVERRIDES.allow_duplicate).openFile(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
const activeLeaf = this.app.workspace.getMostRecentLeaf();
|
||||
if (activeLeaf && this.getAllTabGroups(activeLeaf.getRoot()).length > 1) {
|
||||
menu.addItem((item) => {
|
||||
item.setSection("open");
|
||||
item.setIcon("lucide-split-square-horizontal")
|
||||
item.setTitle(t('menu.openInOppositeTabGroup'));
|
||||
item.onClick(async () => {
|
||||
await this.app.workspace.getLeaf(OVERRIDES.opposite).openFile(file);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
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() {
|
||||
|
|
@ -176,8 +193,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;
|
||||
|
|
@ -208,13 +225,12 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
return async function(this: WorkspaceLeaf, file, openState, ...args) {
|
||||
// openFile doesn't return anything, but just in case that changes.
|
||||
let result: void;
|
||||
let match: 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
|
||||
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);
|
||||
|
|
@ -236,28 +252,30 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
!!openState?.eState?.subpath &&
|
||||
matches.some(l => l.id == openedFrom)
|
||||
);
|
||||
const isMatch = matches.includes(this);
|
||||
|
||||
let match: WorkspaceLeaf|undefined;
|
||||
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 (isInternalLink && !isSpecialOpen && !isMatch) {
|
||||
if (!match && isInternalLink && !isSpecialOpen) {
|
||||
match = matches.find(l => l.id == openedFrom)!;
|
||||
} else if (settings.deduplicateTabs && !isSpecialOpen && matches.length > 0 && !isMatch) {
|
||||
// choose matches first from last opened from, then matches in same group, then fist in list.
|
||||
match = matches.find(l => l.id == openedFrom);
|
||||
if (!match) matches.find(l => l.parent == this.parent);
|
||||
}
|
||||
// choose matches first from last opened from, then matches in same group, then first in list.
|
||||
if (settings.deduplicateTabs && !isSpecialOpen && matches.length > 0) {
|
||||
if (!match) match = matches.find(l => l.id == openedFrom);
|
||||
if (!match) match = matches.find(l => l.parent == this.parent);
|
||||
if (!match) match = matches[0];
|
||||
}
|
||||
|
||||
if (match) {
|
||||
if (match && match !== this) {
|
||||
if (match.view.getViewType() == "kanban") {
|
||||
// workaround for a bug in kanban. See
|
||||
// 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]);
|
||||
plugin.app.workspace.setActiveLeaf(match);
|
||||
result = undefined;
|
||||
} else {
|
||||
const activeLeaf = plugin.app.workspace.getActiveViewOfType(View)?.leaf;
|
||||
result = await oldMethod.call(matches[0], file, {
|
||||
result = await oldMethod.call(match, file, {
|
||||
...openState,
|
||||
active: !!openState?.active || activeLeaf == this,
|
||||
}, ...args);
|
||||
|
|
@ -280,7 +298,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
delete this.openTabSettings;
|
||||
delete this.openTabSettings?.openInfo;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -315,8 +333,27 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async updateSettings(settings: Partial<OpenTabSettingsPluginSettings>) {
|
||||
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)}`)
|
||||
settings.modClickBehavior = 'tab'
|
||||
}
|
||||
|
||||
Object.assign(this.settings, settings);
|
||||
if (!this.settings.previewTabs) {
|
||||
this.app.workspace.iterateAllLeaves(l => this.setLeafIsPreview(l, false));
|
||||
}
|
||||
await this.saveData(this.settings);
|
||||
}
|
||||
|
||||
|
|
@ -340,7 +377,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
|
||||
/**
|
||||
* Gets all tab groups, sorted by active time.
|
||||
* Gets all tab groups.
|
||||
*/
|
||||
private getAllTabGroups(root: WorkspaceItem): TabGroup[] {
|
||||
const tabGroups: Set<TabGroup> = new Set(); // sets are ordered
|
||||
|
|
@ -352,6 +389,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
|
||||
|
|
@ -365,12 +429,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)) {
|
||||
return activeLeaf;
|
||||
}
|
||||
|
||||
let group: TabGroup|undefined;
|
||||
let index: number|undefined;
|
||||
|
||||
|
|
@ -408,13 +466,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)) {
|
||||
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
|
||||
|
|
@ -425,6 +496,7 @@ export default class OpenTabSettingsPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
this.setLeafIsPreview(newLeaf, settings.previewTabs);
|
||||
if (focus) {
|
||||
workspace.setActiveLeaf(newLeaf);
|
||||
}
|
||||
|
|
|
|||
146
src/settings.ts
146
src/settings.ts
|
|
@ -1,29 +1,32 @@
|
|||
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||
import { App, Notice, PluginSettingTab, Setting } from 'obsidian';
|
||||
import OpenTabSettingsPlugin from "./main"
|
||||
import { t } from 'i18next';
|
||||
|
||||
export const NEW_TAB_PLACEMENTS = {
|
||||
"after-active": "After active tab",
|
||||
"after-pinned": "After pinned tabs",
|
||||
"beginning": "At beginning",
|
||||
"end": "At end",
|
||||
"after-active": 'settings.newTabPlacement.options.after-active',
|
||||
"after-pinned": 'settings.newTabPlacement.options.after-pinned',
|
||||
"beginning": 'settings.newTabPlacement.options.beginning',
|
||||
"end": 'settings.newTabPlacement.options.end',
|
||||
};
|
||||
|
||||
export const NEW_TAB_TAB_GROUP_PLACEMENTS = {
|
||||
"same": "In same tab group",
|
||||
"opposite": "In opposite tab group",
|
||||
"first": "In first tab group",
|
||||
"last": "In last tab group",
|
||||
"same": 'settings.newTabTabGroupPlacement.options.same',
|
||||
"opposite": 'settings.newTabTabGroupPlacement.options.opposite',
|
||||
"first": 'settings.newTabTabGroupPlacement.options.first',
|
||||
"last": 'settings.newTabTabGroupPlacement.options.last',
|
||||
};
|
||||
|
||||
export const MOD_CLICK_BEHAVIOR = {
|
||||
"tab": "In new tab",
|
||||
"same": "In same tab",
|
||||
"allow_duplicate": "In duplicate tab",
|
||||
"opposite": "In opposite tab group",
|
||||
"tab": 'settings.modClickBehavior.options.tab',
|
||||
"same": 'settings.modClickBehavior.options.same',
|
||||
"allow_duplicate": 'settings.modClickBehavior.options.allow_duplicate',
|
||||
"opposite": 'settings.modClickBehavior.options.opposite',
|
||||
"no_preview": 'settings.modClickBehavior.options.no_preview',
|
||||
}
|
||||
|
||||
export interface OpenTabSettingsPluginSettings {
|
||||
openInNewTab: boolean,
|
||||
previewTabs: boolean,
|
||||
deduplicateTabs: boolean,
|
||||
deduplicateAcrossTabGroups: boolean,
|
||||
newTabPlacement: keyof typeof NEW_TAB_PLACEMENTS,
|
||||
|
|
@ -33,6 +36,7 @@ export interface OpenTabSettingsPluginSettings {
|
|||
|
||||
export const DEFAULT_SETTINGS: OpenTabSettingsPluginSettings = {
|
||||
openInNewTab: true,
|
||||
previewTabs: false,
|
||||
deduplicateTabs: true,
|
||||
deduplicateAcrossTabGroups: true,
|
||||
newTabPlacement: "after-active",
|
||||
|
|
@ -40,6 +44,24 @@ export const DEFAULT_SETTINGS: OpenTabSettingsPluginSettings = {
|
|||
modClickBehavior: "tab",
|
||||
}
|
||||
|
||||
function translateOptions(options: Record<string, string>): Record<string, string> {
|
||||
return Object.fromEntries(Object.entries(options).map(([value, label]) => [value, t(label)]));
|
||||
}
|
||||
|
||||
const DISABLED_KEY = "open-tab-settings:disabled-on-device";
|
||||
|
||||
export function isDisabledOnDevice(): boolean {
|
||||
return window.localStorage.getItem(DISABLED_KEY) === "true";
|
||||
}
|
||||
|
||||
export function setDisabledOnDevice(value: boolean): void {
|
||||
if (value) {
|
||||
window.localStorage.setItem(DISABLED_KEY, "true");
|
||||
} else {
|
||||
window.localStorage.removeItem(DISABLED_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
|
||||
plugin: OpenTabSettingsPlugin;
|
||||
|
||||
|
|
@ -50,46 +72,55 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
|
|||
|
||||
display(): void {
|
||||
this.containerEl.empty();
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('Always open in new tab')
|
||||
.setDesc('Open files in a new tab by default.')
|
||||
.setName(t('settings.openInNewTab.name'))
|
||||
.setDesc(t('settings.openInNewTab.description'))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.openInNewTab)
|
||||
.onChange(async (value) => {
|
||||
const modClickBehavior = this.plugin.settings.modClickBehavior;
|
||||
await this.plugin.updateSettings({
|
||||
openInNewTab: value,
|
||||
modClickBehavior: (!value && modClickBehavior == "same") ? "tab" : modClickBehavior,
|
||||
});
|
||||
await this.plugin.updateSettings({openInNewTab: value});
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('Prevent duplicate tabs')
|
||||
.setDesc('If a tab is already open, switch to it instead of re-opening it.')
|
||||
.setName(t('settings.previewTabs.name'))
|
||||
.setDesc(t('settings.previewTabs.description'))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.previewTabs)
|
||||
.onChange(async (value) => {
|
||||
await this.plugin.updateSettings({previewTabs: value});
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.setDisabled(!this.plugin.settings.openInNewTab)
|
||||
.settingEl
|
||||
.setCssStyles({opacity: this.plugin.settings.openInNewTab ? "" : "50%"});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName(t('settings.deduplicateTabs.name'))
|
||||
.setDesc(t('settings.deduplicateTabs.description'))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.deduplicateTabs)
|
||||
.onChange(async (value) => {
|
||||
const modClickBehavior = this.plugin.settings.modClickBehavior;
|
||||
await this.plugin.updateSettings({
|
||||
deduplicateTabs: value,
|
||||
modClickBehavior: (!value && modClickBehavior == "allow_duplicate") ? "tab" : modClickBehavior,
|
||||
});
|
||||
await this.plugin.updateSettings({deduplicateTabs: value});
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('Deduplicate across tab groups')
|
||||
.setDesc('Whether to switch to already open file even if its in a split pane or popout window')
|
||||
.setName(t('settings.deduplicateAcrossTabGroups.name'))
|
||||
.setDesc(t('settings.deduplicateAcrossTabGroups.description'))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.plugin.settings.deduplicateAcrossTabGroups)
|
||||
.onChange(async (value) => {
|
||||
await this.plugin.updateSettings({deduplicateAcrossTabGroups: value});
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
.setDisabled(!this.plugin.settings.deduplicateTabs)
|
||||
|
|
@ -97,67 +128,84 @@ export class OpenTabSettingsPluginSettingTab extends PluginSettingTab {
|
|||
.setCssStyles({opacity: this.plugin.settings.deduplicateTabs ? "" : "50%"});
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('Focus explicit new tabs')
|
||||
.setDesc(
|
||||
'Immediately switch to new tabs opened via middle or ctrl click instead of opening them in the ' +
|
||||
'background. New tabs created by regular click will always focus regardless.'
|
||||
)
|
||||
.setName(t('settings.focusNewTabs.name'))
|
||||
.setDesc(t('settings.focusNewTabs.description'))
|
||||
// this is just an alias for Obsidian Settings > Editor > Always focus new tabs
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(this.app.vault.getConfig("focusNewTab") as boolean)
|
||||
.onChange(async (value) => {
|
||||
this.app.vault.setConfig("focusNewTab", value)
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('New tab placement')
|
||||
.setDesc('Place new tabs...')
|
||||
.setName(t('settings.newTabPlacement.name'))
|
||||
.setDesc(t('settings.newTabPlacement.description'))
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOptions(NEW_TAB_PLACEMENTS)
|
||||
.addOptions(translateOptions(NEW_TAB_PLACEMENTS))
|
||||
.setValue(this.plugin.settings.newTabPlacement)
|
||||
.onChange(async value => {
|
||||
await this.plugin.updateSettings({
|
||||
newTabPlacement: value as keyof typeof NEW_TAB_PLACEMENTS,
|
||||
});
|
||||
this.display();
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('New tab tab group placement')
|
||||
.setDesc('When the workspace is split, prefer to open new tabs...')
|
||||
.addDropdown(toggle =>
|
||||
toggle
|
||||
.addOptions(NEW_TAB_TAB_GROUP_PLACEMENTS)
|
||||
.setName(t('settings.newTabTabGroupPlacement.name'))
|
||||
.setDesc(t('settings.newTabTabGroupPlacement.description'))
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOptions(translateOptions(NEW_TAB_TAB_GROUP_PLACEMENTS))
|
||||
.setValue(this.plugin.settings.newTabTabGroupPlacement)
|
||||
.onChange(async (value) => {
|
||||
await this.plugin.updateSettings({
|
||||
newTabTabGroupPlacement: value as keyof typeof NEW_TAB_TAB_GROUP_PLACEMENTS,
|
||||
});
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName('Mod click behavior')
|
||||
.setDesc('On ctrl/cmd/middle click open links...')
|
||||
.setName(t('settings.modClickBehavior.name'))
|
||||
.setDesc(t('settings.modClickBehavior.description'))
|
||||
.addDropdown(dropdown => {
|
||||
dropdown.addOption("tab", MOD_CLICK_BEHAVIOR['tab']);
|
||||
dropdown.addOption("tab", t(MOD_CLICK_BEHAVIOR['tab']));
|
||||
if (this.plugin.settings.openInNewTab) {
|
||||
dropdown.addOption("same", MOD_CLICK_BEHAVIOR['same'])
|
||||
dropdown.addOption("same", t(MOD_CLICK_BEHAVIOR['same']))
|
||||
}
|
||||
if (this.plugin.settings.deduplicateTabs) {
|
||||
dropdown.addOption("allow_duplicate", MOD_CLICK_BEHAVIOR['allow_duplicate'])
|
||||
dropdown.addOption("allow_duplicate", t(MOD_CLICK_BEHAVIOR['allow_duplicate']))
|
||||
}
|
||||
dropdown.addOption("opposite", t(MOD_CLICK_BEHAVIOR['opposite']))
|
||||
if (this.plugin.settings.previewTabs) {
|
||||
dropdown.addOption("no_preview", t(MOD_CLICK_BEHAVIOR['no_preview']))
|
||||
}
|
||||
dropdown.addOption("opposite", MOD_CLICK_BEHAVIOR['opposite'])
|
||||
dropdown
|
||||
.setValue(this.plugin.settings.modClickBehavior)
|
||||
.onChange(async value => {
|
||||
await this.plugin.updateSettings({
|
||||
modClickBehavior: value as keyof typeof MOD_CLICK_BEHAVIOR,
|
||||
});
|
||||
})
|
||||
this.display();
|
||||
});
|
||||
})
|
||||
|
||||
new Setting(this.containerEl)
|
||||
.setName(t('settings.disableOnDevice.name'))
|
||||
.setDesc(t('settings.disableOnDevice.description'))
|
||||
.addToggle(toggle =>
|
||||
toggle
|
||||
.setValue(isDisabledOnDevice())
|
||||
.onChange((value) => {
|
||||
setDisabledOnDevice(value);
|
||||
new Notice(t('settings.disableOnDevice.notice'), 5000);
|
||||
this.display();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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;
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ type LeafInfo = {
|
|||
active: boolean,
|
||||
/** True if this leaf is selected within its tab group */
|
||||
currentTab: boolean,
|
||||
isPreview: boolean,
|
||||
}
|
||||
|
||||
class WorkspacePage {
|
||||
|
|
@ -74,8 +75,8 @@ class WorkspacePage {
|
|||
|
||||
const activeLeaf = app.workspace.getActiveViewOfType(obsidian.View)!.leaf;
|
||||
|
||||
return [...tabGroups].map(leaf =>
|
||||
(leaf.children as WorkspaceLeaf[]).map(leaf => {
|
||||
return tabGroups.map(group =>
|
||||
(group.children as WorkspaceLeaf[]).map(leaf => {
|
||||
return {
|
||||
parent: leaf.parent.id,
|
||||
id: leaf.id,
|
||||
|
|
@ -85,6 +86,7 @@ class WorkspacePage {
|
|||
deferred: leaf.isDeferred, pinned: leaf.pinned,
|
||||
active: activeLeaf == leaf,
|
||||
currentTab: leaf.parent.children.indexOf(leaf) === leaf.parent.currentTab,
|
||||
isPreview: leaf.openTabSettings?.isPreview ?? false,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
|
@ -135,8 +137,19 @@ class WorkspacePage {
|
|||
return equals(actual, matcher);
|
||||
});
|
||||
} catch {}
|
||||
// Call expect again, this will give us nice error messages if value doesn't match.
|
||||
expect(actual).toEqual(matcher);
|
||||
if (!equals(actual, matcher)) {
|
||||
// Show a pretty error message with only the relevant keys
|
||||
const keys = [...new Set(expected.flat().flatMap(l => Object.keys(l)))];
|
||||
const filteredActual = actual.map(group =>
|
||||
group.map(leaf => Object.fromEntries(keys.map(k => [k, leaf[k as keyof LeafInfo]])),
|
||||
));
|
||||
|
||||
throw new Error(
|
||||
"Workspace did not match!\n\n" +
|
||||
"Expected:\n" + JSON.stringify(expected, null, 2) + "\n\n" +
|
||||
"Actual:\n" + JSON.stringify(filteredActual, null, 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -155,8 +168,10 @@ class WorkspacePage {
|
|||
* Pins the specified tab. Note it also focuses the tab.
|
||||
*/
|
||||
async pinTab(pathOrId: string) {
|
||||
await this.setActiveFile(pathOrId);
|
||||
await browser.executeObsidianCommand("workspace:toggle-pin");
|
||||
const leafInfo = await this.getLeaf(pathOrId);
|
||||
await browser.executeObsidian(({app}, leafInfo) => {
|
||||
return app.workspace.getLeafById(leafInfo.id)!.setPinned(true);
|
||||
}, leafInfo);
|
||||
}
|
||||
|
||||
async getLink(text: string): Promise<ChainablePromiseElement> {
|
||||
|
|
@ -174,7 +189,7 @@ class WorkspacePage {
|
|||
}
|
||||
|
||||
async openContextMenu(elem: ChainablePromiseElement) {
|
||||
await this.setConfig('nativeMenus', false);
|
||||
// this requires nativeMenus to be set "false" (see appearance.json)
|
||||
const platform = await obsidianPage.getPlatform();
|
||||
if (platform.isDesktopApp) {
|
||||
await elem.click({button: "right"});
|
||||
|
|
@ -226,6 +241,10 @@ class WorkspacePage {
|
|||
await browser.executeObsidianCommand("switcher:open");
|
||||
await browser.keys(path);
|
||||
await browser.keys(Key.Enter);
|
||||
|
||||
if ((await obsidianPage.getPlatform()).isMobile) {
|
||||
await browser.pause(500); // have to wait a bit for the animation to complete
|
||||
}
|
||||
}
|
||||
|
||||
async openFileViaFileExplorer(path: string): Promise<void> {
|
||||
|
|
@ -249,10 +268,28 @@ class WorkspacePage {
|
|||
}, name, value)
|
||||
}
|
||||
|
||||
async removeFile(file: string) {
|
||||
await browser.executeObsidian(async ({app}, file) => {
|
||||
await app.vault.delete(app.vault.getAbstractFileByPath(file)!);
|
||||
}, file);
|
||||
/** Replaces the leaf's content */
|
||||
async editLeaf(pathOrId: string, text: string) {
|
||||
const leafInfo = await workspacePage.getLeaf(pathOrId);
|
||||
await browser.executeObsidian(async ({app, obsidian}, leafInfo, text) => {
|
||||
const leaf = app.workspace.getLeafById(leafInfo.id)!;
|
||||
app.workspace.setActiveLeaf(leaf, {focus: true});
|
||||
await leaf.view.setState({...leaf.view.getState(), mode: "source"}, {history: false});
|
||||
if (leaf.view instanceof obsidian.MarkdownView) {
|
||||
leaf.view.editor.replaceSelection(text);
|
||||
} else {
|
||||
throw Error("Not a markdown view")
|
||||
}
|
||||
}, leafInfo, text);
|
||||
}
|
||||
|
||||
/** double click on the leaf's tab header (for testing tab preview) */
|
||||
async doubleClickTab(pathOrId: string) {
|
||||
const leafInfo = await workspacePage.getLeaf(pathOrId);
|
||||
await browser.executeObsidian(({app}, leafInfo) => {
|
||||
const elem = app.workspace.getLeafById(leafInfo.id)!.tabHeaderEl;
|
||||
elem.dispatchEvent(new MouseEvent("dblclick", {bubbles: true}));
|
||||
}, leafInfo);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,4 +53,24 @@ describe('Test basic deduplicate', function() {
|
|||
[{file: "Loop.md", active: true, currentTab: true}],
|
||||
]);
|
||||
})
|
||||
|
||||
it('internal link with multiple duplicates', async function() {
|
||||
await workspacePage.setSettings({ openInNewTab: true, deduplicateTabs: true });
|
||||
await workspacePage.openFile("Loop.md");
|
||||
await workspacePage.openFile("Loop.md");
|
||||
const [loop1, loop2] = (await workspacePage.getAllLeaves())[0];
|
||||
|
||||
await workspacePage.setActiveFile(loop2.id);
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "Loop.md", id: loop1.id},
|
||||
{type: "markdown", file: "Loop.md", id: loop2.id, active: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.openLink(await workspacePage.getLink("Loop.md#Subheading"));
|
||||
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "Loop.md", id: loop1.id},
|
||||
{type: "markdown", file: "Loop.md", id: loop2.id, active: true},
|
||||
]]);
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ describe('Test basic deduplicate', function() {
|
|||
it('empty tab', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await browser.executeObsidianCommand("workspace:new-tab");
|
||||
await browser.pause(500) // TODO: not sure why I need a manual pause here
|
||||
await workspacePage.openFileViaQuickSwitcher("A.md")
|
||||
await workspacePage.matchWorkspace([[{type: "markdown", file: "A.md", active: true}]])
|
||||
})
|
||||
|
|
@ -181,6 +182,7 @@ describe('Test basic deduplicate', function() {
|
|||
await workspacePage.openFile("B.md");
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.setActiveFile("A.md");
|
||||
await browser.pause(500) // TODO: not sure why I need a manual pause here
|
||||
await workspacePage.openLinkInNewTab(await workspacePage.getLink("B"));
|
||||
// Should still deduplicate if opened in new tab explicitly
|
||||
await workspacePage.matchWorkspace([[
|
||||
|
|
|
|||
|
|
@ -144,6 +144,24 @@ describe('Test deduplicate for splits and windows', function() {
|
|||
{type: "markdown", file: "A.md"}, {type: "markdown", file: "B.md", active: true},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('deduplicate prefers same tab group', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLinkToRight(await workspacePage.getLink("B"));
|
||||
await workspacePage.setActiveFile("B.md")
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.setActiveFile("B.md")
|
||||
|
||||
await workspacePage.matchWorkspace([
|
||||
[{file: "A.md"}],
|
||||
[{file: "B.md", active: true}, {file: "A.md"}]
|
||||
]);
|
||||
await workspacePage.openLink(await workspacePage.getLink("A"));
|
||||
await workspacePage.matchWorkspace([
|
||||
[{file: "A.md"}],
|
||||
[{file: "B.md"}, {file: "A.md", active: true}],
|
||||
]);
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test deduplicate for misc', function() {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ describe('Test disable options', function() {
|
|||
|
||||
describe('Test disabling the plugin', function() {
|
||||
before(async function() {
|
||||
await obsidianPage.disablePlugin("open-tab-settings");
|
||||
await workspacePage.setConfig('focusNewTab', false);
|
||||
});
|
||||
|
||||
|
|
@ -42,18 +41,19 @@ describe('Test disabling the plugin', function() {
|
|||
await workspacePage.loadPlatformWorkspaceLayout("empty");
|
||||
});
|
||||
|
||||
|
||||
after(async function() {
|
||||
afterEach(async function() {
|
||||
await obsidianPage.enablePlugin("open-tab-settings");
|
||||
});
|
||||
|
||||
it("Test disabling the plugin new tabs", async function() {
|
||||
it("new tabs", async function() {
|
||||
await obsidianPage.disablePlugin("open-tab-settings");
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([[{type: "markdown", file: "B.md", active: true}]]);
|
||||
})
|
||||
|
||||
it("Test disable deduplicateTabs", async function() {
|
||||
it("deduplicateTabs", async function() {
|
||||
await obsidianPage.disablePlugin("open-tab-settings");
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openFile("B.md");
|
||||
await workspacePage.setActiveFile("A.md");
|
||||
|
|
@ -63,6 +63,24 @@ describe('Test disabling the plugin', function() {
|
|||
{type: "markdown", file: "B.md", active: true}, {type: "markdown", file: "B.md"},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('previewTabs', async function() {
|
||||
await workspacePage.setSettings({ openInNewTab: true, previewTabs: true });
|
||||
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
|
||||
await obsidianPage.disablePlugin("open-tab-settings");
|
||||
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: false},
|
||||
]]);
|
||||
})
|
||||
})
|
||||
|
||||
describe('Test menu options', function() {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ describe("Mod click", function() {
|
|||
})
|
||||
beforeEach(async function() {
|
||||
await workspacePage.loadPlatformWorkspaceLayout("empty");
|
||||
await workspacePage.setSettings({ openInNewTab: true, deduplicateTabs: true });
|
||||
await workspacePage.setSettings({ openInNewTab: true, deduplicateTabs: true, previewTabs: false });
|
||||
});
|
||||
|
||||
it('Test mod click same', async function() {
|
||||
|
|
@ -115,7 +115,7 @@ describe("Mod click", function() {
|
|||
]]);
|
||||
});
|
||||
|
||||
it("mode click opposite", async function() {
|
||||
it("mod click opposite", async function() {
|
||||
if ((await obsidianPage.getPlatform()).isMobile) this.skip();
|
||||
await workspacePage.setSettings({
|
||||
openInNewTab: true, deduplicateTabs: false, newTabTabGroupPlacement: "opposite",
|
||||
|
|
@ -131,4 +131,16 @@ describe("Mod click", function() {
|
|||
[{type: "markdown", file: "B.md"}, {type: "markdown", file: "B.md"}]
|
||||
]);
|
||||
})
|
||||
|
||||
it("mod click preview", async function() {
|
||||
await workspacePage.setSettings({ openInNewTab: true, previewTabs: true, deduplicateTabs: false });
|
||||
|
||||
await workspacePage.setSettings({ modClickBehavior: "no_preview" });
|
||||
await workspacePage.openFile("A.md");
|
||||
await (await workspacePage.getLink("B")).click({"button": "middle"});
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: false},
|
||||
]]);
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -208,6 +208,48 @@ describe('Test newTabPlacement', function() {
|
|||
]]);
|
||||
})
|
||||
|
||||
it("doesn't replace active empty pinned tab", async function() {
|
||||
await workspacePage.setSettings({newTabPlacement: "end"});
|
||||
await workspacePage.pinTab((await workspacePage.getAllLeaves())[0][0].id)
|
||||
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "empty", pinned: true, active: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.openFileViaQuickSwitcher("A.md");
|
||||
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "empty", pinned: true},
|
||||
{type: "markdown", file: "A.md", active: true},
|
||||
]]);
|
||||
|
||||
})
|
||||
|
||||
it("doesn't replace end empty pinned tab", async function() {
|
||||
await workspacePage.setSettings({newTabPlacement: "end"});
|
||||
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openFile("Loop.md");
|
||||
await browser.executeObsidianCommand("workspace:new-tab");
|
||||
await workspacePage.pinTab((await workspacePage.getAllLeaves())[0][2].id)
|
||||
await workspacePage.setActiveFile("A.md");
|
||||
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", active: true},
|
||||
{type: "markdown", file: "Loop.md"},
|
||||
{type: "empty", pinned: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.openLinkInNewTab(await workspacePage.getLink("B"));
|
||||
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", active: true},
|
||||
{type: "markdown", file: "Loop.md"},
|
||||
{type: "empty", pinned: true},
|
||||
{type: "markdown", file: "B.md"},
|
||||
]]);
|
||||
})
|
||||
|
||||
it("end doesn't replace middle empty tab", async function() {
|
||||
await workspacePage.setSettings({newTabPlacement: "end"});
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const tests = () => {
|
|||
|
||||
it("Open first file via file explorer works", async function() {
|
||||
await workspacePage.matchWorkspace([[{type: "empty"}]]); // Make sure loadWorkspaceLayout is working
|
||||
await workspacePage.openFileViaQuickSwitcher("A.md");
|
||||
await workspacePage.openFileViaFileExplorer("A.md");
|
||||
await workspacePage.matchWorkspace([[{type: "markdown", file: "A.md", active: true}]]);
|
||||
})
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ const tests = () => {
|
|||
await workspacePage.openFile("A.md");
|
||||
await browser.executeObsidianCommand("workspace:new-tab");
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md"}, {type: "empty", active: true},
|
||||
{type: "markdown", file: "A.md", isPreview: false}, {type: "empty", active: true, isPreview: false},
|
||||
]]);
|
||||
})
|
||||
|
||||
|
|
|
|||
211
test/specs/previewTabs.e2e.ts
Normal file
211
test/specs/previewTabs.e2e.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
import { browser } from '@wdio/globals'
|
||||
import workspacePage from 'test/pageobjects/workspace.page';
|
||||
import { obsidianPage } from 'wdio-obsidian-service';
|
||||
|
||||
|
||||
describe('Preview tabs', function() {
|
||||
beforeEach(async function() {
|
||||
await obsidianPage.resetVault();
|
||||
await workspacePage.loadPlatformWorkspaceLayout("empty");
|
||||
await workspacePage.setSettings({ openInNewTab: true, previewTabs: true, deduplicateTabs: false });
|
||||
await workspacePage.setConfig('focusNewTab', false);
|
||||
});
|
||||
|
||||
it('opens new tabs as preview tabs', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('replaces the preview tab instead of opening a new tab', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.openFileViaQuickSwitcher("D.md");
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "D.md", active: true, isPreview: true},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('promotes previews on edit', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.editLeaf("B.md", "some edit");
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", isPreview: false},
|
||||
]]);
|
||||
|
||||
await workspacePage.openFileViaQuickSwitcher("D.md");
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", isPreview: false},
|
||||
{type: "markdown", file: "D.md", active: true, isPreview: true},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('promotes previews on double click', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.doubleClickTab("B.md");
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", isPreview: false},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('promotes previews on pin', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.pinTab("B.md");
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", isPreview: false},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('promote twice', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.editLeaf("B.md", "some edit");
|
||||
await browser.pause(500)
|
||||
await workspacePage.editLeaf("B.md", "another edit");
|
||||
// edits after the first should be ignored
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", isPreview: false},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('multiple tabs in a group', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.doubleClickTab("B.md");
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", isPreview: false},
|
||||
]]);
|
||||
await workspacePage.setActiveFile("A.md");
|
||||
|
||||
await workspacePage.openFileViaQuickSwitcher("D.md");
|
||||
await workspacePage.openFileViaQuickSwitcher("Loop.md");
|
||||
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "Loop.md", active: true, isPreview: true},
|
||||
{type: "markdown", file: "B.md", isPreview: false},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('open to right does not make a preview tab', async function() {
|
||||
if ((await obsidianPage.getPlatform()).isPhone) this.skip();
|
||||
|
||||
await workspacePage.openFileViaFileExplorer("A.md");
|
||||
|
||||
await workspacePage.openLinkToRight(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([
|
||||
[{type: "markdown", file: "A.md", isPreview: true}],
|
||||
[{type: "markdown", file: "B.md", active: true, isPreview: false}],
|
||||
]);
|
||||
})
|
||||
|
||||
it('can have a single preview tab per group', async function() {
|
||||
if ((await obsidianPage.getPlatform()).isPhone) this.skip();
|
||||
|
||||
await workspacePage.openFileViaFileExplorer("A.md");
|
||||
|
||||
await workspacePage.openLinkToRight(await workspacePage.getLink("B"));
|
||||
await workspacePage.openFileViaFileExplorer("D.md");
|
||||
|
||||
await workspacePage.matchWorkspace([
|
||||
[{file: "A.md", isPreview: true}],
|
||||
[{file: "B.md", isPreview: false}, {file: "D.md", active: true, isPreview: true}],
|
||||
]);
|
||||
|
||||
await workspacePage.openLink(await workspacePage.getLink("Loop"));
|
||||
|
||||
await workspacePage.matchWorkspace([
|
||||
[{file: "A.md", isPreview: true}],
|
||||
[{file: "B.md", isPreview: false}, {file: "Loop.md", active: true, isPreview: true}],
|
||||
]);
|
||||
|
||||
await workspacePage.doubleClickTab("Loop.md")
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
|
||||
await workspacePage.matchWorkspace([
|
||||
[{file: "A.md", isPreview: true}],
|
||||
[
|
||||
{file: "B.md", isPreview: false},
|
||||
{file: "Loop.md", isPreview: false},
|
||||
{file: "B.md", active: true, isPreview: true},
|
||||
],
|
||||
]);
|
||||
})
|
||||
|
||||
it('cleanup on setting disable', async function() {
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.setSettings({ previewTabs: false });
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: false},
|
||||
]]);
|
||||
})
|
||||
|
||||
it('with deduplicate', async function() {
|
||||
await workspacePage.setSettings({ deduplicateTabs: true });
|
||||
|
||||
await workspacePage.openFile("A.md");
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"));
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.openLink(await workspacePage.getLink("A"))
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", active: true, isPreview: false},
|
||||
{type: "markdown", file: "B.md", isPreview: true},
|
||||
]]);
|
||||
|
||||
await workspacePage.openLink(await workspacePage.getLink("B"))
|
||||
await workspacePage.matchWorkspace([[
|
||||
{type: "markdown", file: "A.md", isPreview: false},
|
||||
{type: "markdown", file: "B.md", active: true, isPreview: true},
|
||||
]]);
|
||||
})
|
||||
})
|
||||
3
test/vault/.obsidian/appearance.json
vendored
Normal file
3
test/vault/.obsidian/appearance.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"nativeMenus": false
|
||||
}
|
||||
2
test/vault/.obsidian/core-plugins.json
vendored
2
test/vault/.obsidian/core-plugins.json
vendored
|
|
@ -8,7 +8,7 @@
|
|||
"outgoing-link": true,
|
||||
"tag-pane": true,
|
||||
"properties": false,
|
||||
"page-preview": true,
|
||||
"page-preview": false,
|
||||
"daily-notes": true,
|
||||
"templates": true,
|
||||
"note-composer": true,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
"noImplicitAny": true,
|
||||
"moduleResolution": "node",
|
||||
"importHelpers": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": true,
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
|
|
|
|||
|
|
@ -33,5 +33,8 @@
|
|||
"2.0.0": "1.8.3",
|
||||
"2.0.1": "1.8.3",
|
||||
"2.0.2": "1.8.3",
|
||||
"2.0.3": "1.8.3"
|
||||
"2.0.3": "1.8.3",
|
||||
"2.1.0": "1.8.3",
|
||||
"2.1.1": "1.8.3",
|
||||
"2.2.0": "1.8.7"
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ export const config: WebdriverIO.Config = {
|
|||
services: [
|
||||
"obsidian",
|
||||
["appium", {
|
||||
args: { allowInsecure: "chromedriver_autodownload,adb_shell" },
|
||||
args: { allowInsecure: "*:chromedriver_autodownload,*:adb_shell" },
|
||||
}],
|
||||
],
|
||||
reporters: ["obsidian"],
|
||||
|
|
|
|||
Loading…
Reference in a new issue