Skip to content

Commit

Permalink
Update workspace API and detection behavior, add support for "dynamic…
Browse files Browse the repository at this point in the history
…" workspaces (#366)

* Update workspace API and detection behavior

Also removes the `detect_cwd` configuration option, which was not
well-defined before anyway and is no longer relevant given the new
behavior.

* Add link to PR in deprecation warning

* Improve other error messages

* Remove ref to `detect_cwd` in README

* Add support for "no path" workspaces

* fix test

* path is a function

* CHANGELOG, documentation

* Clean up

* Log workspace switch at debug level

* Clean up comments

* clean up
  • Loading branch information
epwalsh committed Feb 6, 2024
1 parent 32de09d commit 7c1ff20
Show file tree
Hide file tree
Showing 10 changed files with 432 additions and 260 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Added

- Added support for "dynamic" workspaces where the workspace `path` field is a function instead of a `string` / `Path`. See [PR #366](https://github.com/epwalsh/obsidian.nvim/pull/366).

### Changed

- Changed behavior of workspace detection. When you have multiple workspaces configured, obsidian.nvim will now automatically switch workspaces when you open a buffer in a different workspace. See [PR #366](https://github.com/epwalsh/obsidian.nvim/pull/366).
- Various changes to the `Workspace` Lua API. See [PR #366](https://github.com/epwalsh/obsidian.nvim/pull/366).

### Removed

- Removed configuration option `detect_cwd`. This wasn't well-defined before and is no longer relevant with the new workspace detection behavior. See [PR #366](https://github.com/epwalsh/obsidian.nvim/pull/366).

## [v2.10.0](https://github.com/epwalsh/obsidian.nvim/releases/tag/v2.10.0) - 2024-02-05

### Added
Expand Down
54 changes: 46 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ _Keep in mind this plugin is not meant to replace Obsidian, but to complement it
- [Configuration options](#configuration-options)
- [Notes on configuration](#notes-on-configuration)
- [Using templates](#using-templates)
- [Using obsidian.nvim outside of a workspace / Obsidian vault](#using-obsidiannvim-outside-of-a-workspace--obsidian-vault)
- 🐞 [Known issues](#known-issues)
-[Contributing](#contributing)

Expand Down Expand Up @@ -210,10 +211,9 @@ This is a complete list of all of the options that can be passed to `require("ob

```lua
{
-- A list of vault names and paths.
-- Each path should be the path to the vault root. If you use the Obsidian app,
-- the vault root is the parent directory of the `.obsidian` folder.
-- You can also provide configuration overrides for each workspace through the `overrides` field.
-- A list of workspace names, paths, and configuration overrides.
-- If you use the Obsidian app, the 'path' of a workspace should generally be
-- your vault root (where the `.obsidian` folder is located).
workspaces = {
{
name = "personal",
Expand All @@ -233,10 +233,6 @@ This is a complete list of all of the options that can be passed to `require("ob
-- 'workspaces'. For example:
-- dir = "~/vaults/work",

-- Optional, set to true to use the current directory as a vault; otherwise
-- the first workspace is opened by default.
detect_cwd = false,

-- Optional, if you keep notes in a specific subdirectory of your vault.
notes_subdir = "notes",

Expand Down Expand Up @@ -608,6 +604,48 @@ templates = {
}
```

### Using obsidian.nvim outside of a workspace / Obsidian vault

It's possible to configure obsidian.nvim to work on individual markdown files outside of a regular workspace / Obsidian vault by configuring a "dynamic" workspace. To do so you just need to add a special workspace with a function for the `path` field (instead of a string), which should return a *parent* directory of the current buffer. This tells obsidian.nvim to use that directory as the workspace `path` and `root` (vault root) when the buffer is not located inside another fixed workspace.

For example, to extend the configuration above this way:

```diff
{
workspaces = {
{
name = "personal",
path = "~/vaults/personal",
},
...
+ {
+ name = "no-vault",
+ path = function()
+ -- alternatively use the CWD:
+ -- return assert(vim.fn.getcwd())
+ return assert(vim.fs.dirname(vim.api.nvim_buf_get_name(0)))
+ end,
+ overrides = {
+ notes_subdir = vim.NIL, -- have to use 'vim.NIL' instead of 'nil'
+ completion = {
+ new_notes_location = "current_dir",
+ },
+ templates = {
+ subdir = vim.NIL,
+ },
+ disable_frontmatter = true,
+ },
+ },
+ },
...
}
```

With this configuration, anytime you enter a markdown buffer outside of "~/vaults/personal" (or whatever your configured fixed vaults are), obsidian.nvim will switch to the dynamic workspace with the path / root set to the parent directory of the buffer.

Please note that in order to avoid unexpected behavior (like a new directory being created for `notes_subdir`) it's important to carefully set the workspace `overrides` options.
And keep in mind that to reset a configuration option to `nil` you'll have to use `vim.NIL` there instead of the builtin Lua `nil` due to the way Lua tables work.

## Known Issues

### Configuring vault directory behind a link
Expand Down
5 changes: 3 additions & 2 deletions lua/cmp_obsidian_new.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local abc = require "obsidian.abc"
local completion = require "obsidian.completion.refs"
local obsidian = require "obsidian"
local log = require "obsidian.log"
local NewNotesLocation = require("obsidian.config").CompletionNewNotesLocation
local LinkStyle = require("obsidian.config").LinkStyle

---@class cmp_obsidian_new.Source : obsidian.ABC
Expand All @@ -23,12 +24,12 @@ source.complete = function(_, request, callback)
local dir
if client.opts.completion.new_notes_location == nil then
dir = nil -- let the client decide
elseif client.opts.completion.new_notes_location == "notes_subdir" then
elseif client.opts.completion.new_notes_location == NewNotesLocation.notes_subdir then
dir = client.dir
if client.opts.notes_subdir ~= nil then
dir = dir / client.opts.notes_subdir
end
elseif client.opts.completion.new_notes_location == "current_dir" then
elseif client.opts.completion.new_notes_location == NewNotesLocation.current_dir then
dir = vim.fn.expand "%:p:h"
else
log.err "Bad option value for 'completion.new_notes_location'. Skipping creating new note."
Expand Down
94 changes: 73 additions & 21 deletions lua/obsidian/client.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
--- *obsidian-api*
--- *obidian-api*
---
--- The Obsidian.nvim Lua API.
---
Expand Down Expand Up @@ -87,6 +87,10 @@ Client.new = function(opts)
self._quiet = false

local workspace = Workspace.get_from_opts(opts)
if not workspace then
error "At least one workspace is required!\nPlease specify a workspace in your Obsidian.nvim config."
end

self:set_workspace(workspace)

if self.opts.yaml_parser ~= nil then
Expand All @@ -101,7 +105,33 @@ end
Client.set_workspace = function(self, workspace)
self.current_workspace = workspace
self.dir = self:vault_root(workspace)
self.opts = config.ClientOpts.normalize(self._default_opts, workspace.overrides)
self.opts = self:opts_for_workspace(workspace)

-- Ensure directories exist.
self.dir:mkdir { parents = true, exists_ok = true }

if self.opts.notes_subdir ~= nil then
local notes_subdir = self.dir / self.opts.notes_subdir
notes_subdir:mkdir { parents = true, exists_ok = true }
end

if self.opts.daily_notes.folder ~= nil then
local daily_notes_subdir = self.dir / self.opts.daily_notes.folder
daily_notes_subdir:mkdir { parents = true, exists_ok = true }
end
end

--- Get the normalize opts for a given workspace.
---
---@param workspace obsidian.Workspace|?
---
---@return obsidian.config.ClientOpts
Client.opts_for_workspace = function(self, workspace)
if workspace then
return config.ClientOpts.normalize(workspace.overrides and workspace.overrides or {}, self._default_opts)
else
return self.opts
end
end

--- Switch to a different workspace.
Expand All @@ -116,13 +146,13 @@ Client.switch_workspace = function(self, workspace)

for _, ws in ipairs(self.opts.workspaces) do
if ws.name == workspace then
return self:switch_workspace(ws)
return self:switch_workspace(Workspace.new_from_spec(ws))
end
end

error(string.format("Workspace '%s' not found", workspace))
else
if workspace.path == self.current_workspace.path then
if workspace == self.current_workspace then
log.info("Already in workspace '%s' @ '%s'", workspace.name, workspace.path)
return
end
Expand All @@ -132,28 +162,43 @@ Client.switch_workspace = function(self, workspace)
end
end

--- Get the absolute path to the root of the Obsidian vault for the given workspace or the
--- current workspace.
--- Check if a path represents a note in the workspace.
---
---@param path string|Path
---@param workspace obsidian.Workspace|?
---
---@return Path
Client.vault_root = function(self, workspace)
workspace = workspace and workspace or self.current_workspace
---@return boolean
Client.path_is_note = function(self, path, workspace)
path = vim.fs.normalize(tostring(path))

local vault_indicator_folder = ".obsidian"
local dirs = Path:new(workspace.path):parents()
table.insert(dirs, 1, workspace.path)
-- Notes have to be markdown file.
if not vim.endswith(path, ".md") then
return false
end

for _, dirpath in ipairs(dirs) do
local dir = Path:new(dirpath)
local maybe_vault = dir / vault_indicator_folder
if maybe_vault:is_dir() then
return dir
-- Ignore markdown files in the templates directory.
local templates_dir = self:templates_dir(workspace)
if templates_dir ~= nil then
local templates_pattern = tostring(templates_dir)
templates_pattern = util.escape_magic_characters(templates_pattern)
templates_pattern = "^" .. templates_pattern .. ".*"
if string.find(path, templates_pattern) then
return false
end
end

return Path:new(workspace.path)
return true
end

--- Get the absolute path to the root of the Obsidian vault for the given workspace or the
--- current workspace.
---
---@param workspace obsidian.Workspace|?
---
---@return Path
Client.vault_root = function(self, workspace)
workspace = workspace and workspace or self.current_workspace
return Path:new(workspace.root)
end

--- Get the name of the current vault.
Expand Down Expand Up @@ -190,10 +235,17 @@ end

--- Get the templates folder.
---
---@param workspace obsidian.Workspace|?
---
---@return Path|?
Client.templates_dir = function(self)
if self.opts.templates ~= nil and self.opts.templates.subdir ~= nil then
local templates_dir = self.dir / self.opts.templates.subdir
Client.templates_dir = function(self, workspace)
local opts = self.opts
if workspace and workspace ~= self.current_workspace then
opts = self:opts_for_workspace(workspace)
end

if opts.templates ~= nil and opts.templates.subdir ~= nil then
local templates_dir = self:vault_root(workspace) / opts.templates.subdir
if not templates_dir:is_dir() then
log.err("'%s' is not a valid directory for templates", templates_dir)
return nil
Expand Down
2 changes: 1 addition & 1 deletion lua/obsidian/commands/workspace.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ local log = require "obsidian.log"

---@param client obsidian.Client
return function(client, data)
if not data.args or #data.args == 0 then
if not data.args or string.len(data.args) == 0 then
log.info("Current workspace: '%s' @ '%s'", client.current_workspace.name, client.current_workspace.path)
return
else
Expand Down
Loading

0 comments on commit 7c1ff20

Please sign in to comment.