Skip to content

Commit 627af65

Browse files
committed
feat(attach): add attachment links
This provides functionality provided by the following functions in the Emacs implementation: - `org-attach-file-list` - `org-attach-expand` - `org-attach-follow` - `org-attach-complete-link`
1 parent c682921 commit 627af65

File tree

8 files changed

+144
-8
lines changed

8 files changed

+144
-8
lines changed

docs/configuration.org

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,16 +1207,31 @@ is available.
12071207

12081208
Attachment inheritance for the outline.
12091209

1210-
Enabling inheritance implies that running =attach= inside a node without
1211-
attachments will operate on the first parent headline that has an
1212-
attachment.
1210+
Enabling inheritance implies two things:
1211+
1. Attachment links will look through all parent headlines until they find
1212+
the linked attachment.
1213+
2. Running =attach= inside a node without attachments will operate on the
1214+
first parent headline that has an attachment.
12131215

12141216
Possible values are:
12151217

12161218
- =always= - inherit attachments
12171219
- =selective= - respect [[#org_use_property_inheritance][org_use_property_inheritance]] for the properties =DIR= and =ID=
12181220
- =never= - don't inherit attachments
12191221

1222+
*** org_attach_store_link_p
1223+
:PROPERTIES:
1224+
:CUSTOM_ID: org_attach_store_link_p
1225+
:END:
1226+
- Type: ='attached' | 'file' | 'original' | false=
1227+
- Default: ='attached'=
1228+
1229+
If not =false=, store a link with [[#org_store_link][org_store_link]] when attaching a file.
1230+
1231+
- =attach= - store a =[[attachment:name]]= link
1232+
- =file= - store a =[[file:attach_dir/name]]= link
1233+
- =original= - store a =[[file:original/location]]= link
1234+
12201235
*** org_attach_id_to_path_function_list
12211236
:PROPERTIES:
12221237
:CUSTOM_ID: org_attach_id_to_path_function_list
@@ -2944,6 +2959,14 @@ the =DIR= property. See also [[#org_attach_id_dir][org_attach_id_dir]],
29442959
[[#org_attach_id_to_path_function_list][org_attach_id_to_path_function_list]] and [[#org_attach_use_inheritance][org_attach_use_inheritance]] on how
29452960
to further customize the attachments directory.
29462961

2962+
Attachment links are supported. A link like =[[attachment:file.txt]]=
2963+
looks up =file.txt= in the current node's attachments directory and opens
2964+
it. Attaching a file stores a link to the attachment. See
2965+
[[#org_attach_store_link_p][org_attach_store_link_p]] on how to configure this behavior.
2966+
2967+
The only missing feature is expansion of attachment links before exporting
2968+
a file with [[#org_export][org_exporting]].
2969+
29472970
** User interface
29482971
:PROPERTIES:
29492972
:CUSTOM_ID: user-interface

lua/orgmode/attach/core.lua

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ local utils = require('orgmode.utils')
66

77
---@class OrgAttachCore
88
---@field files OrgFiles
9+
---@field links OrgLinks
910
local AttachCore = {}
1011
AttachCore.__index = AttachCore
1112

12-
---@param opts {files:OrgFiles}
13+
---@param opts {files:OrgFiles, links:OrgLinks}
1314
function AttachCore.new(opts)
1415
local data = {
1516
files = opts and opts.files,
17+
links = opts and opts.links,
1618
}
1719
return setmetatable(data, AttachCore)
1820
end
@@ -288,6 +290,8 @@ function AttachCore:attach(node, file, opts)
288290
return nil
289291
end
290292
node:add_auto_tag()
293+
local link = self.links:store_link_to_attachment({ attach_dir = attach_dir, original = file })
294+
vim.fn.setreg(vim.v.register, link)
291295
return basename
292296
end)
293297
end)
@@ -314,6 +318,14 @@ function AttachCore:attach_buffer(node, bufnr, opts)
314318
local data = table.concat(vim.api.nvim_buf_get_lines(bufnr, 0, -1, false), '\n')
315319
return utils.writefile(attach_file, data, { excl = true }):next(function()
316320
node:add_auto_tag()
321+
-- Ignore all errors here, this is just to determine whether we can store
322+
-- a link to `bufname`.
323+
local bufname_exists = vim.uv.fs_stat(bufname)
324+
local link = self.links:store_link_to_attachment({
325+
attach_dir = attach_dir,
326+
original = bufname_exists and bufname or attach_file,
327+
})
328+
vim.fn.setreg(vim.v.register, link)
317329
return basename
318330
end)
319331
end)
@@ -341,7 +353,10 @@ function AttachCore:attach_many(node, files, opts)
341353
.mapSeries(function(to_be_attached)
342354
local basename = basename_safe(to_be_attached)
343355
local attach_file = vim.fs.joinpath(attach_dir, basename)
344-
return attach(to_be_attached, attach_file)
356+
return attach(to_be_attached, attach_file):next(function(success)
357+
self.links:store_link_to_attachment({ attach_dir = attach_dir, original = to_be_attached })
358+
return success
359+
end)
345360
end, files)
346361
---@param successes boolean[]
347362
:next(function(successes)

lua/orgmode/attach/init.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ local MAX_TIMEOUT = 2 ^ 31
1414
local Attach = {}
1515
Attach.__index = Attach
1616

17-
---@param opts {files:OrgFiles}
17+
---@param opts {files:OrgFiles, links:OrgLinks}
1818
function Attach:new(opts)
1919
local data = setmetatable({ core = Core.new(opts) }, self)
20+
data.core.links:add_type(require('orgmode.org.links.types.attachment'):new({ attach = data }))
2021
return data
2122
end
2223

lua/orgmode/config/_meta.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@
245245
---@field org_attach_method 'mv' | 'cp' | 'ln' | 'lns' Default method of attacahing files. Default: 'cp'
246246
---@field org_attach_visit_command string | fun(dir: string) Command or Lua function used to open a directory. Default: 'edit'
247247
---@field org_attach_use_inheritance 'always' | 'selective' | 'never' Determines whether headlines inherit the attachments directory of their parents. Default: 'selective'
248+
---@field org_attach_store_link_p 'original' | 'file' | 'attached' | false If true, attaching a file stores a link to it. Default: 'attached'
248249
---@field org_attach_id_to_path_function_list (string | fun(id: string): (string|nil))[] List of functions used to derive the attachments directory from an ID property.
249250
---@field org_attach_sync_delete_empty_dir 'always' | 'ask' | 'never' Determines whether to delete empty directories when using `org.attach.sync()`. Default: 'ask'
250251
---@field win_split_mode? 'horizontal' | 'vertical' | 'auto' | 'float' | string[] How to open agenda and capture windows. Default: 'horizontal'

lua/orgmode/config/defaults.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ local DefaultConfig = {
7878
org_attach_copy_directory_create_symlink = false,
7979
org_attach_visit_command = 'edit',
8080
org_attach_use_inheritance = 'selective',
81+
org_attach_store_link_p = 'attached',
8182
org_attach_id_to_path_function_list = {
8283
'uuid_folder_format',
8384
'ts_folder_format',

lua/orgmode/init.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ function Org:init()
5959
})
6060
:load_sync(true, 20000)
6161
self.links = require('orgmode.org.links'):new({ files = self.files })
62-
self.attach = require('orgmode.attach'):new({ files = self.files })
62+
self.attach = require('orgmode.attach'):new({ files = self.files, links = self.links })
6363
self.agenda = require('orgmode.agenda'):new({
6464
files = self.files,
6565
highlighter = self.highlighter,

lua/orgmode/org/links/init.lua

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,11 @@ function OrgLinks:autocomplete(link)
7676
end
7777

7878
---@param headline OrgHeadline
79+
---@return string url
7980
function OrgLinks:store_link_to_headline(headline)
80-
self.stored_links[self:get_link_to_headline(headline)] = headline:get_title()
81+
local url = self:get_link_to_headline(headline)
82+
self.stored_links[url] = headline:get_title()
83+
return url
8184
end
8285

8386
---@param headline OrgHeadline
@@ -110,6 +113,41 @@ function OrgLinks:get_link_to_file(file)
110113
return ('file:%s::*%s'):format(file.filename, title)
111114
end
112115

116+
---@param params {attach_dir: string, original: string}
117+
---@return string | nil url
118+
function OrgLinks:store_link_to_attachment(params)
119+
local url = self:get_link_to_attachment(params)
120+
if url then
121+
self.stored_links[url] = vim.fs.basename(params.original)
122+
end
123+
return url
124+
end
125+
126+
---@param params {attach_dir: string, original: string}
127+
---@return string | nil url
128+
function OrgLinks:get_link_to_attachment(params)
129+
vim.validate({
130+
attach_dir = { params.attach_dir, 'string' },
131+
original = { params.original, 'string' },
132+
})
133+
local basename = vim.fs.basename(params.original)
134+
local choice = config.org_attach_store_link_p
135+
if choice == 'attached' then
136+
return string.format('attachment:%s', basename)
137+
elseif choice == 'file' then
138+
local attach_file = vim.fs.joinpath(params.attach_dir, basename)
139+
return string.format('file:%s', attach_file)
140+
elseif choice == 'original' then
141+
-- Sanity check: `original` might be a URL. Check for that and return it
142+
-- unmodified if yes.
143+
if params.original:match('^[A-Za-z]+://') then
144+
return params.original
145+
end
146+
return string.format('file:%s', params.original)
147+
end
148+
return nil
149+
end
150+
113151
---@param link_location string
114152
function OrgLinks:insert_link(link_location, desc)
115153
local selected_link = OrgHyperlink:new(link_location)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
---@class OrgLinkAttachment:OrgLinkType
2+
---@field private attach OrgAttach
3+
local OrgLinkAttachment = {}
4+
OrgLinkAttachment.__index = OrgLinkAttachment
5+
6+
---@param opts { attach: OrgAttach }
7+
function OrgLinkAttachment:new(opts)
8+
local this = setmetatable({
9+
attach = opts.attach,
10+
}, OrgLinkAttachment)
11+
return this
12+
end
13+
14+
---@return string
15+
function OrgLinkAttachment:get_name()
16+
return 'attachment'
17+
end
18+
19+
---@param link string
20+
---@return boolean
21+
function OrgLinkAttachment:follow(link)
22+
local opts = self:_parse(link)
23+
if not opts then
24+
return false
25+
end
26+
self.attach:open(opts.basename, opts.node)
27+
return true
28+
end
29+
30+
---@param link string
31+
---@return string[]
32+
function OrgLinkAttachment:autocomplete(link)
33+
local opts = self:_parse(link)
34+
if not opts then
35+
return {}
36+
end
37+
local complete = self.attach:make_completion({ node = opts.node })
38+
return vim.tbl_map(function(name)
39+
return 'attachment:' .. name
40+
end, complete(opts.basename))
41+
end
42+
43+
---@private
44+
---@param link string
45+
---@return { node: OrgAttachNode, basename: string } | nil
46+
function OrgLinkAttachment:_parse(link)
47+
local basename = link:match('^attachment:(.+)$')
48+
if not basename then
49+
return nil
50+
end
51+
return {
52+
node = self.attach:get_current_node(),
53+
basename = basename,
54+
}
55+
end
56+
57+
return OrgLinkAttachment

0 commit comments

Comments
 (0)