Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 14 additions & 5 deletions lua/cdin/runtime/command/defaults.lua
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ function M.register(runtime)
description = "Save a document snapshot atomically through Backend filesystem services",
available = function(context, args)
local document = active_document(runtime, context)
local has_path = document ~= nil
-- Keep path-backed save commands executable even when the optional Host
-- writer is absent. Saver:save owns the typed UNSUPPORTED error; hiding
-- the command here would incorrectly turn Ctrl+S into an unhandled key.
return document ~= nil
and (document.path ~= nil or (args and args.path ~= nil))
local has_atomic_writer = type(runtime.host.fs) == "table"
and type(runtime.host.fs.begin_atomic_write) == "function"
return has_path and has_atomic_writer
end,
})

Expand Down Expand Up @@ -159,11 +159,20 @@ function M.register(runtime)
description = "Open exact file bytes through the optional Backend Host filesystem service",
})

commands:register("document.close", function(context)
commands:register("document.close", function(context, args)
local document = active_document(runtime, context)
if not document then
return false, "no active document"
end
args = args or {}
-- Never discard the only canonical copy of an edited document implicitly.
-- The UI can offer Save / Discard / Cancel; trusted callers must opt into
-- destructive close explicitly with { discard = true }.
if document:is_clean() ~= true and args.discard ~= true then
errors.raise("UNSAVED_CHANGES",
"document has unsaved changes; save it or explicitly request discard",
{ document_id = document.id })
end
return runtime:_close_document(document.id, false)
end, {
description = "Close the focused document and remove its views",
Expand Down
1 change: 1 addition & 0 deletions lua/cdin/runtime/core/error.lua
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ M.codes = {
CANCELLED = true,
SHUTDOWN = true,
UNSUPPORTED = true,
UNSAVED_CHANGES = true,
BUFFER_TOO_SMALL = true,
VERSION_MISMATCH = true,
INTERNAL = true,
Expand Down
41 changes: 38 additions & 3 deletions tests/integration/document_lifecycle_commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,23 @@ A.equal(new_document.id, created.result.document_id)
A.truthy(new_document ~= nil)
A.equal(new_document.length, 0)
local new_id = new_document.id
local close_key = runtime:handle_frontend_event({
type = "input.key", key = "w", action = "press", modifiers = { "ctrl" },
A.equal(runtime:handle_frontend_event({ type = "input.text", text = "unsaved" }).status, "inserted")
A.falsy(new_document:is_clean())
A.error_code(function()
runtime:handle_frontend_event({
type = "input.key", key = "w", action = "press", modifiers = { "ctrl" },
})
end, "UNSAVED_CHANGES")
-- A rejected close must leave both the canonical document and its view alive.
A.equal(runtime.documents.documents[new_id], new_document)
A.truthy(runtime.ui.views[created.result.view_id] ~= nil)
A.falsy(new_document:is_clean())
local discarded = runtime:handle_frontend_event({
type = "request.command", name = "document.close", arguments = { discard = true },
})
A.equal(close_key.status, "executed")
A.equal(discarded.status, "executed")
A.equal(runtime.documents.documents[new_id], nil)
A.equal(runtime.ui.views[created.result.view_id], nil)

runtime:close_document(reopened_document.id)
A.truthy(runtime:shutdown())
Expand All @@ -131,6 +143,29 @@ A.equal(#no_reader_runtime.documents:list(), 0)
A.equal(next(no_reader_runtime.ui.views), nil)
no_reader_runtime:shutdown()

-- A missing atomic writer must surface the typed UNSUPPORTED failure.
-- Ctrl+S must not silently become an unhandled key when a document has a path.
local no_writer_host = FakeHost.new()
no_writer_host.fs.begin_atomic_write = nil
local no_writer_runtime = Runtime.new({ host = no_writer_host })
no_writer_runtime:start()
local no_writer_document = no_writer_runtime:open_document("original", { path = path })
no_writer_runtime:create_view(no_writer_document.id)
no_writer_document:replace(0, 0, "edit", {
expected_version = no_writer_document.version,
})
A.falsy(no_writer_document:is_clean())
A.error_code(function()
no_writer_runtime:handle_frontend_event({
type = "input.key", key = "s", action = "press", modifiers = { "ctrl" },
})
end, "UNSUPPORTED")
A.falsy(no_writer_document:is_clean())
A.equal(#no_writer_runtime.documents:list(), 1)
A.equal(no_writer_host.fs:read_test_file(path), nil)
no_writer_runtime:close_document(no_writer_document.id)
A.truthy(no_writer_runtime:shutdown())

local missing_file_host = FakeHost.new()
local missing_file_runtime = Runtime.new({ host = missing_file_host })
missing_file_runtime:start()
Expand Down
Loading