├── preview.gif ├── versions.json ├── styles.css ├── .vscode └── tasks.json ├── manifest.json ├── tsconfig.json ├── .gitignore ├── rollup.config.js ├── package.json ├── LICENSE ├── README.md └── main.ts /preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jsartelle/obsidian-auto-split/HEAD/preview.gif -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.2": "1.0", 3 | "1.1.2": "0.12.19", 4 | "1.1.1": "0.12.19", 5 | "1.1.0": "0.12.19", 6 | "1.0.1": "0.12.19", 7 | "1.0.0": "0.12.19" 8 | } 9 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .auto-split-settings-info-text { 2 | display: flex; 3 | align-items: center; 4 | gap: 0.75em; 5 | } 6 | 7 | .auto-split-settings-info-text svg { 8 | flex-shrink: 0; 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "dev", 7 | "problemMatcher": [], 8 | "label": "npm: dev", 9 | "detail": "rollup --config rollup.config.js -w", 10 | "runOptions": {"runOn": "folderOpen"} 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-auto-split", 3 | "name": "Auto Split", 4 | "version": "1.2", 5 | "minAppVersion": "0.12.19", 6 | "description": "Open notes with side-by-side editor & preview", 7 | "author": "James Sartelle", 8 | "authorUrl": "https://github.com/jsartelle", 9 | "isDesktopOnly": false 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "es6", 8 | "allowJs": true, 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "lib": [ 13 | "dom", 14 | "es5", 15 | "scripthost", 16 | "es2015" 17 | ] 18 | }, 19 | "include": [ 20 | "**/*.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # General 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | # Icon must end with two \r 7 | Icon 8 | 9 | # Thumbnails 10 | ._* 11 | 12 | # Files that might appear in the root of a volume 13 | .DocumentRevisions-V100 14 | .fseventsd 15 | .Spotlight-V100 16 | .TemporaryItems 17 | .Trashes 18 | .VolumeIcon.icns 19 | .com.apple.timemachine.donotpresent 20 | 21 | # Directories potentially created on remote AFP share 22 | .AppleDB 23 | .AppleDesktop 24 | Network Trash Folder 25 | Temporary Items 26 | .apdisk 27 | 28 | # Intellij 29 | *.iml 30 | .idea 31 | 32 | # npm 33 | node_modules 34 | 35 | # build 36 | main.js 37 | *.js.map 38 | 39 | # obsidian 40 | data.json 41 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import {nodeResolve} from '@rollup/plugin-node-resolve'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | 5 | const isProd = (process.env.BUILD === 'production'); 6 | 7 | const banner = 8 | `/* 9 | THIS IS A GENERATED/BUNDLED FILE BY ROLLUP 10 | if you want to view the source visit the plugins github repository 11 | */ 12 | `; 13 | 14 | export default { 15 | input: 'main.ts', 16 | output: { 17 | dir: '.', 18 | sourcemap: 'inline', 19 | sourcemapExcludeSources: isProd, 20 | format: 'cjs', 21 | exports: 'default', 22 | banner, 23 | }, 24 | external: ['obsidian'], 25 | plugins: [ 26 | typescript(), 27 | nodeResolve({browser: true}), 28 | commonjs(), 29 | ] 30 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-auto-split", 3 | "version": "1.2", 4 | "description": "Open notes with split editor & preview", 5 | "type": "module", 6 | "main": "main.js", 7 | "scripts": { 8 | "dev": "rollup --config rollup.config.js -w", 9 | "build": "rollup --config rollup.config.js --environment BUILD:production" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@rollup/plugin-commonjs": "^26.0.1", 16 | "@rollup/plugin-node-resolve": "^15.2.3", 17 | "@rollup/plugin-typescript": "^11.1.6", 18 | "@types/node": "^22.2.0", 19 | "obsidian": "^1.6.6", 20 | "rollup": "^4.20.0", 21 | "tslib": "^2.6.3", 22 | "typescript": "^5.5.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2021 James Sartelle 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Auto Split 2 | 3 | Opens notes with the editor & preview side-by-side when your workspace is empty. Also adds a command to split the current pane into two linked panes. 4 | 5 | **Only works on desktop & tablet devices - Obsidian 1.0+ doesn't support split panes on phones.** 6 | 7 | ![](https://raw.githubusercontent.com/jsartelle/obsidian-auto-split/master/preview.gif) 8 | 9 | ### Changelog 10 | 11 | - 1.2: 2024-08-04 12 | - Added: "Split and link current pane" command 13 | - Changed: disable on phones 14 | - Changed: clean up settings and update copy 15 | - Changed: code cleanup 16 | - Changed: update dependencies 17 | - 1.1.2: 2022-01-30 18 | - Fixed: prevent splitting if there are already files open 19 | - 1.1.1: 2022-01-12 20 | - Changed: only activate on Markdown files 21 | - 1.1.0: 2021-12-10 22 | - Added: setting to disable on specific platforms 23 | - Changed: improved description 24 | - Changed: improved settings 25 | - 1.0.1: 2021-11-15 26 | - Fixed: add null check on activeLeaf 27 | - Fixed: remove extraneous onLayoutReady 28 | - 1.0.0: 2021-10-29 29 | - Initial release -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Platform, 4 | Plugin, 5 | PluginSettingTab, 6 | Setting, 7 | MarkdownView, 8 | setIcon, 9 | type TFile, 10 | } from 'obsidian' 11 | 12 | type SplitDirectionSetting = 'vertical' | 'horizontal' | 'auto' 13 | type PaneTypeSetting = 'source' | 'preview' 14 | 15 | interface AutoSplitSettings { 16 | autoSplit: boolean 17 | minSize: number 18 | direction: SplitDirectionSetting 19 | editorFirst: boolean 20 | paneToFocus: PaneTypeSetting 21 | linkPanes: boolean 22 | } 23 | 24 | const DEFAULT_SETTINGS: AutoSplitSettings = { 25 | autoSplit: true, 26 | minSize: 1000, 27 | direction: 'auto', 28 | editorFirst: true, 29 | paneToFocus: 'source', 30 | linkPanes: true, 31 | } 32 | 33 | export default class AutoSplitPlugin extends Plugin { 34 | settings!: AutoSplitSettings 35 | 36 | protected hasOpenFiles = false 37 | protected updateHasOpenFiles() { 38 | try { 39 | this.hasOpenFiles = 40 | this.app.workspace.getLeavesOfType('markdown').length > 0 41 | } catch (e) { 42 | // it's okay to fail sometimes 43 | } 44 | } 45 | 46 | async onload() { 47 | await this.loadSettings() 48 | 49 | this.addSettingTab(new AutoSplitSettingTab(this.app, this)) 50 | 51 | this.addCommand({ 52 | id: 'split-current-pane', 53 | name: 'Split and link current pane', 54 | checkCallback: (checking) => { 55 | if (Platform.isPhone) return false 56 | const file = this.app.workspace.activeEditor?.file 57 | if (!file) return false 58 | if (!checking) this.splitActiveFile(file) 59 | return true 60 | }, 61 | }) 62 | 63 | this.app.workspace.onLayoutReady(() => { 64 | this.updateHasOpenFiles() 65 | 66 | this.registerEvent( 67 | this.app.workspace.on('file-open', async (file) => { 68 | if ( 69 | this.settings.autoSplit && 70 | !Platform.isPhone && 71 | this.app.workspace.getLeavesOfType('markdown').length === 1 && 72 | !this.hasOpenFiles && 73 | file 74 | ) { 75 | await this.splitActiveFile(file, true) 76 | } 77 | 78 | this.updateHasOpenFiles() 79 | }) 80 | ) 81 | }) 82 | } 83 | 84 | async loadSettings() { 85 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()) 86 | } 87 | 88 | async saveSettings() { 89 | await this.saveData(this.settings) 90 | } 91 | 92 | async splitActiveFile(file: TFile, autoSplit = false) { 93 | const activeLeaf = 94 | this.app.workspace.getActiveViewOfType(MarkdownView)?.leaf 95 | if (!activeLeaf) return 96 | 97 | const rootSize = getRootContainerSize(this.app) 98 | let direction = this.settings.direction 99 | if (direction === 'auto') { 100 | direction = rootSize.width >= rootSize.height ? 'vertical' : 'horizontal' 101 | } 102 | 103 | if ( 104 | (direction === 'vertical' ? rootSize.width : rootSize.height) > 105 | this.settings.minSize 106 | ) { 107 | const viewState = activeLeaf.getViewState() 108 | 109 | if (viewState.type !== 'markdown') return 110 | 111 | viewState.active = false 112 | viewState.state.mode = 113 | viewState.state.mode === 'preview' ? 'source' : 'preview' 114 | 115 | const firstPane = this.settings.editorFirst ? 'source' : 'preview' 116 | 117 | const newLeaf = this.app.workspace.createLeafBySplit( 118 | activeLeaf, 119 | direction, 120 | autoSplit && viewState.state.mode === firstPane 121 | ) 122 | await newLeaf.openFile(file, viewState) 123 | 124 | if (!autoSplit || this.settings.linkPanes) { 125 | activeLeaf.setGroupMember(newLeaf) 126 | } 127 | 128 | if (autoSplit && viewState.state.mode === this.settings.paneToFocus) { 129 | this.app.workspace.setActiveLeaf(newLeaf, { focus: true }) 130 | } 131 | } 132 | } 133 | } 134 | 135 | class AutoSplitSettingTab extends PluginSettingTab { 136 | plugin: AutoSplitPlugin 137 | 138 | constructor(app: App, plugin: AutoSplitPlugin) { 139 | super(app, plugin) 140 | this.plugin = plugin 141 | } 142 | 143 | display(): void { 144 | let { containerEl } = this 145 | 146 | containerEl.empty() 147 | 148 | if (Platform.isPhone) { 149 | const infoText = containerEl.createEl('div', { 150 | cls: 'auto-split-settings-info-text', 151 | }) 152 | setIcon(infoText, 'info') 153 | infoText.createEl('p', { 154 | text: 'Split panes are not supported on phones.', 155 | }) 156 | return 157 | } 158 | 159 | containerEl.createEl('h2', { text: 'Auto Split Settings' }) 160 | 161 | new Setting(containerEl) 162 | .setName('Split Automatically') 163 | .setDesc( 164 | 'Turn off to only split when the command "Split and link current pane" is used.' 165 | ) 166 | .addToggle((toggle) => { 167 | toggle 168 | .setValue(this.plugin.settings.autoSplit) 169 | .onChange(async (value) => { 170 | this.plugin.settings.autoSplit = value 171 | await this.plugin.saveSettings() 172 | }) 173 | }) 174 | 175 | const { width: rootWidth, height: rootHeight } = getRootContainerSize( 176 | this.app 177 | ) 178 | 179 | new Setting(containerEl) 180 | .setName('Minimum Size') 181 | .setDesc( 182 | `Only split if the main area is at least this wide or tall, depending on split direction. The main area was ${rootWidth}x${rootHeight} when you opened this screen. (default: 1000)` 183 | ) 184 | .addText((text) => { 185 | text.inputEl.type = 'number' 186 | text 187 | .setValue(String(this.plugin.settings.minSize)) 188 | .onChange(async (value) => { 189 | const valueAsNumber = Number.parseInt(value) 190 | this.plugin.settings.minSize = Number.isInteger(valueAsNumber) 191 | ? valueAsNumber 192 | : this.plugin.settings.minSize 193 | await this.plugin.saveSettings() 194 | }) 195 | }) 196 | 197 | new Setting(containerEl) 198 | .setName('Split Direction') 199 | .setDesc( 200 | 'Vertical = left/right, Horizontal = up/down. Auto splits vertically if the main area is wider than it is tall, and horizontally otherwise.' 201 | ) 202 | .addDropdown((dropdown) => { 203 | dropdown 204 | .addOptions({ 205 | auto: 'Auto', 206 | vertical: 'Vertical', 207 | horizontal: 'Horizontal', 208 | }) 209 | .setValue(this.plugin.settings.direction) 210 | .onChange(async (value) => { 211 | this.plugin.settings.direction = value as SplitDirectionSetting 212 | await this.plugin.saveSettings() 213 | }) 214 | }) 215 | 216 | const infoText = containerEl.createEl('div', { 217 | cls: 'auto-split-settings-info-text', 218 | }) 219 | setIcon(infoText, 'info') 220 | infoText.createEl('p', { 221 | text: 'Settings below do not apply to the "Split and link current pane" command.', 222 | }) 223 | 224 | new Setting(containerEl) 225 | .setName('Editor First') 226 | .setDesc('Place the pane with the editor on the left/top.') 227 | .addToggle((toggle) => { 228 | toggle 229 | .setValue(this.plugin.settings.editorFirst) 230 | .onChange(async (value) => { 231 | this.plugin.settings.editorFirst = value 232 | await this.plugin.saveSettings() 233 | }) 234 | }) 235 | 236 | new Setting(containerEl) 237 | .setName('Focus On') 238 | .setDesc('Select which pane should be focused.') 239 | .addDropdown((dropdown) => { 240 | dropdown 241 | .addOptions({ 242 | source: 'Editor', 243 | preview: 'Preview', 244 | }) 245 | .setValue(this.plugin.settings.paneToFocus) 246 | .onChange(async (value) => { 247 | this.plugin.settings.paneToFocus = value as PaneTypeSetting 248 | await this.plugin.saveSettings() 249 | }) 250 | }) 251 | 252 | new Setting(containerEl) 253 | .setName('Link Panes') 254 | .setDesc( 255 | 'Link the panes so their scroll position and open file stay the same.' 256 | ) 257 | .addToggle((toggle) => { 258 | toggle 259 | .setValue(this.plugin.settings.linkPanes) 260 | .onChange(async (value) => { 261 | this.plugin.settings.linkPanes = value 262 | await this.plugin.saveSettings() 263 | }) 264 | }) 265 | } 266 | } 267 | 268 | function getRootContainerSize(app: App) { 269 | const rootContainer: HTMLElement = app.workspace.rootSplit.doc.documentElement 270 | 271 | if (rootContainer) { 272 | return { 273 | width: rootContainer.clientWidth, 274 | height: rootContainer.clientHeight, 275 | } 276 | } else { 277 | console.warn(`[Auto Split] couldn't get root container, using window size`) 278 | return { 279 | width: window.innerWidth, 280 | height: window.innerHeight, 281 | } 282 | } 283 | } 284 | --------------------------------------------------------------------------------