Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support workspaces config option #155

Merged
merged 31 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
41cfd7f
feat: support workspaces config option
weskoerber Jul 21, 2023
070ef9f
chore: revert changes to doc/obsidian.txt
weskoerber Jul 21, 2023
2aaf7ff
refactor: default_workspace and workspaces table
weskoerber Aug 3, 2023
d3313a3
feat: add command to switch workspace
weskoerber Aug 3, 2023
ee7a3f7
chore: run stylua
weskoerber Aug 3, 2023
9c10132
chore: update changelog
weskoerber Aug 3, 2023
731fd49
fix: not setting workspace in new_from_dir
weskoerber Aug 4, 2023
8d71474
chore: sync doc changes with main
weskoerber Aug 4, 2023
c5a3a8f
chore: maintain current workspace in client
weskoerber Aug 4, 2023
170dddb
refactor: workspace command changes
weskoerber Aug 4, 2023
9c19046
chore: commit stylua changes
weskoerber Aug 4, 2023
591a116
chore: update readme
weskoerber Aug 4, 2023
d780e10
fix: default_workspace type in readme
weskoerber Aug 4, 2023
acd5f23
chore: workspace class
weskoerber Aug 8, 2023
a5931ba
refactor: use workspace class
weskoerber Aug 8, 2023
c31a654
fix: run luacheck and stylua
weskoerber Aug 8, 2023
d3a93c5
chore: update readme
weskoerber Aug 8, 2023
417eb91
fix: passing wrong table to vim.tbl_extend
weskoerber Aug 8, 2023
c741398
chore: clarify table type in workspace class
weskoerber Aug 9, 2023
46b4a83
chore: add workspace tests
weskoerber Aug 9, 2023
598bc0a
chore: add comments clarifying workspace path normalization
weskoerber Oct 17, 2023
48ae1f1
chore: revert changes to doc/obsidian.txt to upstream/main
weskoerber Oct 17, 2023
e190304
chore: add function to create new workspace from dir
weskoerber Oct 17, 2023
1e1ba52
chore: default workspace name to the name of the directory
weskoerber Oct 17, 2023
04dd838
fix: backward incompatibility
weskoerber Oct 17, 2023
852895f
chore: run stylua and luacheck
weskoerber Oct 17, 2023
79118e1
refactor: use dir name for default vault instead of "."
weskoerber Oct 17, 2023
f422f98
fix: backward incompatibility
weskoerber Oct 17, 2023
9402d59
chore: update readme
weskoerber Oct 17, 2023
bc228a0
fix: failed test using wrong workspace name for current directory
weskoerber Oct 17, 2023
242fdbe
update type hints
epwalsh Oct 18, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- The `overwrite_mappings` option, which sets the mappings in the config even if they already exist
- Added support for multiple vaults (#128)
- Added command to switch between vaults (#60)

### Fixed

- Eliminated silent runtime errors on validation errors in note.from_lines
- Eliminated silent runtime errors on validation errors in note.from_lines

## [v1.14.2](https://github.com/epwalsh/obsidian.nvim/releases/tag/v1.14.2) - 2023-09-25

Expand Down
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Built for people who love the concept of Obsidian -- a simple, markdown-based no
This command has one optional argument: the ID, path, or alias of the note to link to. If not given, the selected text will be used to find the note with a matching ID, path, or alias.
- `:ObsidianLinkNew` to create a new note and link it to an in-line visual selection of text.
This command has one optional argument: the title of the new note. If not given, the selected text will be used as the title.
- `:ObsidianWorkspace` to switch to another workspace.

### Demo

Expand Down Expand Up @@ -86,7 +87,16 @@ return {
-- see below for full list of optional dependencies 👇
},
opts = {
dir = "~/my-vault", -- no need to call 'vim.fn.expand' here
workspaces = {
{
name = "personal",
path = "~/vaults/personal",
},
{
name = "work",
path = "~/vaults/work",
},
},

-- see below for full list of options 👇
},
Expand All @@ -106,7 +116,16 @@ use({
},
config = function()
require("obsidian").setup({
dir = "~/my-vault",
workspaces = {
{
name = "personal",
path = "~/vaults/personal",
},
{
name = "work",
path = "~/vaults/work",
},
},

-- see below for full list of options 👇
})
Expand All @@ -132,8 +151,23 @@ This is a complete list of all of the options that can be passed to `require("ob

```lua
{
-- Required, the path to your vault directory.
dir = "~/my-vault",
-- Optional, and for backward compatibility. Setting this will use it as the default workspace
-- dir = "~/vaults/other",
-- Optional, list of vault names and paths.
workspaces = {
{
name = "personal",
path = "~/vaults/personal",
},
{
name = "work",
path = "~/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
29 changes: 29 additions & 0 deletions lua/obsidian/command.lua
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,34 @@ command.check_health = function(client, _)
end
end

command.switch_workspace = function(client, data)
if not data.args or #data.args == 0 then
echo.info(
"Current workspace: " .. client.current_workspace.name .. " @ " .. tostring(client.dir),
client.opts.log_level
)
return
end

local workspace = nil
for _, value in pairs(client.opts.workspaces) do
if data.args == value.name then
workspace = value
end
end

if not workspace then
echo.err("Workspace '" .. data.args .. "' does not exist", client.opts.log_level)
return
end

client.current_workspace = workspace

echo.info("Switching to workspace '" .. workspace.name .. "' (" .. workspace.path .. ")", client.opts.log_level)
-- NOTE: workspace.path has already been normalized
client.dir = Path:new(workspace.path)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same suggestion here about normalizing:

Suggested change
client.dir = Path:new(workspace.path)
client.dir = Path:new(vim.fs.normalize(tostring(workspace.path)))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment in a previous suggestion

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok fair enough! Could you add a comment here at least:

Suggested change
client.dir = Path:new(workspace.path)
-- NOTE: workspace.path has already been normalized
client.dir = Path:new(workspace.path)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with 598bc0a

end

local commands = {
ObsidianCheck = { func = command.check, opts = { nargs = 0 } },
ObsidianTemplate = { func = command.template, opts = { nargs = "?" } },
Expand All @@ -665,6 +693,7 @@ local commands = {
ObsidianLinkNew = { func = command.link_new, opts = { nargs = "?", range = true } },
ObsidianFollowLink = { func = command.follow, opts = { nargs = 0 } },
ObsidianCheckHealth = { func = command.check_health, opts = { nargs = 0 } },
ObsidianWorkspace = { func = command.switch_workspace, opts = { nargs = "?" } },
}

---Register all commands.
Expand Down
19 changes: 16 additions & 3 deletions lua/obsidian/config.lua
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to keep backwards compatibility for when people upgrade. This should be fairly easy:

Just check if dir is set instead of workspaces and convert dir into a workspace object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f422f98

Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
local echo = require "obsidian.echo"
local workspace = require "obsidian.workspace"

local config = {}

---[[ Options specs ]]---

---@class obsidian.config.ClientOpts
---@field dir string
---@field dir string|?
---@field workspaces obsidian.Workspace[]|?
---@field detect_cwd boolean
---@field log_level integer|?
---@field notes_subdir string|?
---@field templates obsidian.config.TemplateOpts
Expand All @@ -30,7 +33,9 @@ config.ClientOpts = {}
---@return obsidian.config.ClientOpts
config.ClientOpts.default = function()
return {
dir = vim.fs.normalize "./",
dir = nil,
workspaces = {},
detect_cwd = false,
log_level = nil,
notes_subdir = nil,
templates = config.TemplateOpts.default(),
Expand Down Expand Up @@ -64,13 +69,21 @@ config.ClientOpts.normalize = function(opts)
opts.mappings = opts.mappings and opts.mappings or config.MappingOpts.default()
opts.daily_notes = vim.tbl_extend("force", config.DailyNotesOpts.default(), opts.daily_notes)
opts.templates = vim.tbl_extend("force", config.TemplateOpts.default(), opts.templates)
opts.dir = vim.fs.normalize(tostring(opts.dir))

-- Validate.
if opts.sort_by ~= nil and not vim.tbl_contains({ "path", "modified", "accessed", "created" }, opts.sort_by) then
echo.err("invalid 'sort_by' option '" .. opts.sort_by .. "'")
end

for key, value in pairs(opts.workspaces) do
opts.workspaces[key].path = vim.fs.normalize(tostring(value.path))
end

if opts.dir ~= nil then
-- NOTE: path will be normalized in workspace.new() fn
table.insert(opts.workspaces, 1, workspace.new("dir", opts.dir))
end

return opts
end

Expand Down
9 changes: 7 additions & 2 deletions lua/obsidian/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ obsidian.completion = require "obsidian.completion"
obsidian.note = require "obsidian.note"
obsidian.util = require "obsidian.util"
obsidian.mapping = require "obsidian.mapping"
obsidian.workspace = require "obsidian.workspace"

---@class obsidian.Client
---@field current_workspace obsidian.Workspace
---@field dir Path
---@field templates_dir Path|?
---@field opts obsidian.config.ClientOpts
Expand All @@ -24,7 +26,10 @@ local client = {}
---@return obsidian.Client
obsidian.new = function(opts)
local self = setmetatable({}, { __index = client })
self.dir = Path:new(vim.fs.normalize(tostring(opts.dir and opts.dir or "./")))

self.current_workspace = obsidian.workspace.get_from_opts(opts)
-- NOTE: workspace.path has already been normalized
self.dir = Path:new(self.current_workspace.path)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should still normalize dir to avoid breaking changes.

Suggested change
self.dir = Path:new(self.current_workspace.path)
self.dir = Path:new(vim.fs.normalize(tostring(self.current_workspace.path)))

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! I finally have time to swing back around on these...

The workspace paths should already be normalized.

Here, in workspace.new(), the path is normalized:

workspace.new = function(name, path)
  local self = setmetatable({}, { __index = workspace })

  self.name = name
  self.path = vim.fs.normalize(path)

  return self
end

Paths are also normalized when reading from the config in client.ClientOpts.normalize():

  for key, value in pairs(opts.workspaces) do
    opts.workspaces[key].path = vim.fs.normalize(tostring(value.path))
  end

There could be an edge-case I haven't fully considered, but I think it's safe to omit the redundant path normalization. However, if you still prefer normalizing the paths where you suggested I won't argue with you 😄

self.opts = opts
self.backlinks_namespace = vim.api.nvim_create_namespace "ObsidianBacklinks"

Expand All @@ -37,7 +42,7 @@ end
---@return obsidian.Client
obsidian.new_from_dir = function(dir)
local opts = config.ClientOpts.default()
opts.dir = vim.fs.normalize(dir)
opts.workspaces = vim.tbl_extend("force", { obsidian.workspace.new_from_dir(dir) }, opts.workspaces)
return obsidian.new(opts)
end

Expand Down
14 changes: 14 additions & 0 deletions lua/obsidian/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ util.contains = function(table, val)
return false
end

---Check if a table (list) contains a key.
---
---@param table table
---@param needle any
---@return boolean
util.contains_key = function(table, needle)
for key, _ in pairs(table) do
if key == needle then
return true
end
end
return false
end

---Return a new table (list) with only the unique values of the original.
---
---@param table table
Expand Down
74 changes: 74 additions & 0 deletions lua/obsidian/workspace.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---@class obsidian.Workspace
---@field name string
---@field path string
local workspace = {}

---Create a new workspace
---
---@param name string Workspace name
---@param path string Workspace path (will be normalized)
---
---@return obsidian.Workspace
workspace.new = function(name, path)
local self = setmetatable({}, { __index = workspace })

self.name = name
self.path = vim.fs.normalize(path)

return self
end

workspace.new_from_cwd = function()
return workspace.new_from_dir(vim.fn.getcwd())
end

workspace.new_from_dir = function(dir)
return workspace.new(vim.fn.fnamemodify(dir, ":t"), dir)
end

---Determines if cwd is a workspace
---
---@param workspaces table<obsidian.Workspace>
---@return obsidian.Workspace|nil
workspace.get_workspace_from_cwd = function(workspaces)
local cwd = vim.fn.getcwd()
local _, value = next(vim.tbl_filter(function(w)
if w.path == cwd then
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to normalize these paths before comparing them, otherwise it seems a bit brittle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment in a previous suggestion

return true
end
return false
end, workspaces))

return value
end

---Returns the default workspace
---
---@param workspaces table<obsidian.Workspace>
---@return obsidian.Workspace|nil
workspace.get_default_workspace = function(workspaces)
local _, value = next(workspaces)
return value
end

---Resolves current workspace from client config
---
---@param opts obsidian.config.ClientOpts
---@return obsidian.Workspace
workspace.get_from_opts = function(opts)
local current_workspace

if opts.detect_cwd then
current_workspace = workspace.get_workspace_from_cwd(opts.workspaces)
else
current_workspace = workspace.get_default_workspace(opts.workspaces)
end

if not current_workspace then
current_workspace = workspace.new_from_cwd()
end

return current_workspace
end

return workspace
64 changes: 64 additions & 0 deletions test/obsidian/workspace_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
local workspace = require "obsidian.workspace"

local opts = {
workspaces = {
{
name = "work",
path = "~/notes/work",
},
{
name = "personal",
path = "~/notes/personal",
},
{
name = "cwd_workspace",
path = os.getenv "PWD",
},
},
detect_cwd = false,
}

describe("Workspace", function()
it("should be able to initialize a workspace", function()
local ws = workspace.new("test_workspace", "/tmp/obsidian_test_workspace")
assert.equals("test_workspace", ws.name)
assert.equals("/tmp/obsidian_test_workspace", ws.path)
end)

it("should be able to initialize from cwd", function()
local ws = workspace.new_from_cwd()
local cwd = os.getenv "PWD"
assert.equals(vim.fn.fnamemodify(vim.fn.getcwd(), ":t"), ws.name)
assert.equals(cwd, ws.path)
end)

it("should be able to retrieve the default workspace", function()
local ws = workspace.get_default_workspace(opts.workspaces)
assert.is_not(ws, nil)
assert.equals(opts.workspaces[1].name, ws.name)
assert.equals(opts.workspaces[1].path, ws.path)
end)

it("should initialize workspace from cwd", function()
local ws = workspace.get_workspace_from_cwd(opts.workspaces)
assert.equals(opts.workspaces[3].name, ws.name)
assert.equals(opts.workspaces[3].path, ws.path)
end)

it("should return cwd workspace when detect_cwd is true", function()
local old_cwd = opts.detect_cwd
opts.detect_cwd = true
local ws = workspace.get_from_opts(opts)
assert.equals(opts.workspaces[3].name, ws.name)
assert.equals(opts.workspaces[3].path, ws.path)
opts.detect_cwd = old_cwd
end)
it("should return default workspace when detect_cwd is false", function()
local old_cwd = opts.detect_cwd
opts.detect_cwd = false
local ws = workspace.get_from_opts(opts)
assert.equals(opts.workspaces[1].name, ws.name)
assert.equals(opts.workspaces[1].path, ws.path)
opts.detect_cwd = old_cwd
end)
end)