Skip to content

Commit

Permalink
feat: Integrate BufWriteCmd for "write" command (#2116)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiyaowong committed Jun 22, 2024
1 parent 6837142 commit 8606eb1
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 67 deletions.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,16 @@ versions for the best experience. These can be found

### VSCode specific differences

- File and editor management commands such as `:e`/`:w`/`:q`/`:vsplit`/`:tabnext`/etc are mapped to corresponding
VSCode commands and behavior may be different ([see below](#%EF%B8%8F--keybindings-shortcuts)).
- **Do not** use vim commands like `:w` in scripts/keybindings, they won't work. If you're using them in some
- File and editor management commands such as `:e`/`:q`/`:vsplit`/`:tabnext`/etc are mapped to corresponding VSCode
commands and behavior may be different ([see below](#%EF%B8%8F--keybindings-shortcuts)).
- **Do not** use vim commands like `:e` in scripts/keybindings, they won't work. If you're using them in some
custom commands/mappings, you might need to rebind them to call VSCode commands from Neovim with
`require('vscode').call()` (see [API](#%EF%B8%8F-api)).
- When you type some commands they may be substituted for another, like `:write` will be replaced by `:Write`.
- Since version 1.18.0, `:w`, `:wa` and `:sav` commands are supported and no longer alias to VSCode commands. You
can use them as you would in Neovim.
- When you type some commands they may be substituted for another, check
[AlterCommand](https://github.com/search?q=repo%3Avscode-neovim%2Fvscode-neovim%20AlterCommand&type=code) for the
list of substitutions.
- Scrolling is done by VSCode. <kbd>C-d</kbd>/<kbd>C-u</kbd>/etc are slightly different.
- Editor customization (relative line number, scrolloff, etc) is handled by VSCode.
- Dot-repeat (<kbd>.</kbd>) is slightly different - moving the cursor within a change range won't break the repeat.
Expand Down Expand Up @@ -793,8 +797,8 @@ visible, press K again to focus the hover widget.

### File management

The extension aliases various Nvim commands (`:edit`, `:enew`, `:find`, `:write`, `:saveas`, `:wall`, `:quit`, etc.) to
equivalent vscode commands. Also their normal-mode equivalents (where applicable) such as <kbd>C-w q</kbd>, etc.
The extension aliases various Nvim commands (`:edit`, `:enew`, `:find`, `:quit`, etc.) to equivalent vscode commands.
Also their normal-mode equivalents (where applicable) such as <kbd>C-w q</kbd>, etc.

> 💡 See [Keybindings help](#keybindings-help) to see all defined shortcuts and their documentation.
Expand Down
71 changes: 71 additions & 0 deletions runtime/lua/vscode/internal.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
---@diagnostic disable: deprecated
local api, fn = vim.api, vim.fn

local vscode = require("vscode.api")
local util = require("vscode.util")

local M = {}
Expand Down Expand Up @@ -280,4 +282,73 @@ function M.wslpath(path)
return vim.trim(ret)
end

--#region Buffer management

--- Implements :write and related commands, via buftype=acwrite. #521 #1260
---
local function set_buffer_autocmd(buf)
api.nvim_create_autocmd({ "BufWriteCmd" }, {
buffer = buf,
callback = function(ev)
local current_name = vim.api.nvim_buf_get_name(ev.buf)
local target_name = ev.match
local data = {
buf = ev.buf,
bang = vim.v.cmdbang == 1,
current_name = current_name,
target_name = target_name,
}
vscode.action("save_buffer", { args = { data } })
end,
})
end

---@class InitDocumentBufferData
---@field buf number
---@field lines string[]
---@field editor_options EditorOptions
---@field uri string
---@field uri_data table
---@field modifiable boolean
---@field bufname string
---@field modified boolean

---@param data InitDocumentBufferData
function M.init_document_buffer(data)
local buf = data.buf

-- Set bufname first so that the filetype detection can work ???
api.nvim_buf_set_name(buf, data.bufname)
api.nvim_buf_set_lines(buf, 0, -1, false, data.lines)
-- set vscode controlled flag so we can check it neovim
api.nvim_buf_set_var(buf, "vscode_controlled", true)
-- In vscode same document can have different insertSpaces/tabSize settings
-- per editor; in Nvim it's per buffer. We assume here that these settings are
-- same for all editors.
api.nvim_buf_set_var(buf, "vscode_editor_options", data.editor_options)
api.nvim_buf_set_var(buf, "vscode_uri", data.uri)
api.nvim_buf_set_var(buf, "vscode_uri_data", data.uri_data)
-- force acwrite, which is similar to nofile, but will only be written via the
-- BufWriteCmd autocommand. #521 #1260
api.nvim_buf_set_option(buf, "buftype", "acwrite")
api.nvim_buf_set_option(buf, "buflisted", true)
api.nvim_buf_set_option(buf, "modifiable", data.modifiable)
api.nvim_buf_set_option(buf, "modified", false)

set_buffer_autocmd(buf)
end

---Reset undo tree for a buffer
-- Called from extension when opening/creating new file in vscode to reset undo tree
function M.clear_undo(buf)
local mod = vim.bo[buf].modified
local ul = vim.bo[buf].undolevels
api.nvim_buf_set_option(buf, "undolevels", -1)
api.nvim_buf_set_lines(buf, 0, 0, false, {})
api.nvim_buf_set_option(buf, "undolevels", ul)
api.nvim_buf_set_option(buf, "modified", mod)
end

--#endregion

return M
9 changes: 0 additions & 9 deletions runtime/vscode-neovim.vim
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,6 @@ function! VSCodeNotifyRangePos(cmd, line1, line2, pos1, pos2, leaveSelection, ..
\ [a:cmd, [a:line1 - 1, a:pos1 - 1, a:line2 - 1, a:pos2 - 1], a:leaveSelection ? v:false : v:true, a:000])
endfunction

" Called from extension when opening/creating new file in vscode to reset undo tree
function! VSCodeClearUndo(bufId)
let oldlevels = &undolevels
call nvim_buf_set_option(a:bufId, 'undolevels', -1)
call nvim_buf_set_lines(a:bufId, 0, 0, 0, [])
call nvim_buf_set_option(a:bufId, 'undolevels', oldlevels)
unlet oldlevels
endfunction

function! s:onInsertEnter()
let reg = reg_recording()
if !empty(reg)
Expand Down
7 changes: 0 additions & 7 deletions runtime/vscode/overrides/vscode-file-commands.vim
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ command! -bang -nargs=? Ex call <SID>editOrNew(<q-args>, <q-bang>)
command! -bang Enew call <SID>editOrNew('__vscode_new__', <q-bang>)
command! -bang Find call VSCodeNotify('workbench.action.quickOpen')

command! -complete=file -bang -nargs=? Write if <q-bang> ==# '!' | call VSCodeNotify('workbench.action.files.saveAs') | else | call VSCodeNotify('workbench.action.files.save') | endif
command! -bang Saveas call VSCodeNotify('workbench.action.files.saveAs')

command! -bang Wall call VSCodeNotify('workbench.action.files.saveAll')
command! -bang Quit if <q-bang> ==# '!' | call VSCodeNotify('workbench.action.revertAndCloseActiveEditor') | else | call VSCodeNotify('workbench.action.closeActiveEditor') | endif

command! -bang Wq call <SID>saveAndClose()
Expand All @@ -49,9 +45,6 @@ AlterCommand e[dit] Edit
AlterCommand ex Ex
AlterCommand ene[w] Enew
AlterCommand fin[d] Find
AlterCommand w[rite] Write
AlterCommand sav[eas] Saveas
AlterCommand wa[ll] Wall
AlterCommand q[uit] Quit
AlterCommand wq Wq
AlterCommand x[it] Xit
Expand Down
168 changes: 126 additions & 42 deletions src/buffer_manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { config } from "./config";
import { EventBusData, eventBus } from "./eventBus";
import { createLogger } from "./logger";
import { MainController } from "./main_controller";
import { ManualPromise, convertByteNumToCharNum, disposeAll, wait } from "./utils";
import { ManualPromise, convertByteNumToCharNum, disposeAll, fileExists, wait } from "./utils";

// NOTE: document and editors in vscode events and namespace are reference stable
// Integration notes:
Expand Down Expand Up @@ -142,6 +142,7 @@ export class BufferManager implements Disposable {
window.onDidChangeActiveTextEditor(this.onEditorLayoutChanged),
workspace.onDidCloseTextDocument(this.onEditorLayoutChanged),
workspace.onDidCloseNotebookDocument(this.onEditorLayoutChanged),
workspace.onDidSaveTextDocument(() => this.syncDocumentDirtyState()),
window.onDidChangeTextEditorOptions((e) => this.onDidChangeEditorOptions(e.textEditor)),
workspace.registerTextDocumentContentProvider(BUFFER_SCHEME, this.bufferProvider),
eventBus.on("redraw", this.handleRedraw, this),
Expand Down Expand Up @@ -173,6 +174,8 @@ export class BufferManager implements Disposable {
editor.options = { tabSize, insertSpaces, lineNumbers };
},
);

actions.add("save_buffer", (data) => this.handleSaveBuf(data));
}

public dispose(): void {
Expand Down Expand Up @@ -266,9 +269,7 @@ export class BufferManager implements Disposable {
} else {
const normalizedName = fileName.trim();
let uri = Uri.from({ scheme: "file", path: this.findPathFromFileName(normalizedName) });
try {
await workspace.fs.stat(uri);
} catch {
if (!(await fileExists(uri))) {
uri = Uri.from({ scheme: "untitled", path: normalizedName });
// Why notebook?
// Limitations with TextDocument, specifically when there is no active
Expand Down Expand Up @@ -425,6 +426,104 @@ export class BufferManager implements Disposable {

private handleWindowChangedDebounced = debounce(this.handleWindowChanged, 100, { leading: false, trailing: true });

private async syncDocumentDirtyState(): Promise<void> {
const states = Array.from(this.textDocumentToBufferId.entries()).map(([doc, bufId]) => ({
buf: bufId,
modified: doc.isDirty,
}));
await this.client.lua(
`
local states = ...
for _, state in ipairs(states) do
vim.bo[state.buf].modified = state.modified
end
`,
[states],
);
}

private async handleSaveBuf({
buf,
bang,
current_name,
target_name,
}: {
buf: number;
bang: boolean;
current_name: string;
target_name: string;
}) {
// Note:
// 1. The approach here is to compute a generic relative file path using
// Vim's data first, then integrate it with VSCode's working directory.
// - Compute the relative-path using vim-target-filepath and vim-cwd.
// - Compute the vscode-target-filepath using this relative-path and vscode-cwd.
//
// 2. workspace.save and workspace.saveAs are smart enough to handle the
// documents that are not a real file (e.g. untitled, output, etc.)
// so we can just call them directly

const document = this.getTextDocumentForBufferId(buf);
if (document == null) {
throw new Error(`Cannot save buffer ${buf} - ${target_name}`);
}

const docUri = document.uri;

if (document.isUntitled) {
await workspace.save(docUri);
return;
}

// If using Windows locally and developing on a Unix remote environment,
// the saved path can contain backslashes, causing folders to be treated as filenames.
const normalize = (p: string) => path.normalize(p).split(path.sep).join(path.posix.sep);

const currentPath = normalize(current_name);
const targetPath = normalize(target_name);

if (currentPath === targetPath) {
await workspace.save(docUri);
return;
}

const vimCwd = normalize(await this.main.client.call("getcwd"));
const relativePath = normalize(path.relative(vimCwd, targetPath));

if (relativePath === targetPath) {
// Who wanna do this rare thing?
// e.g. cwd: c:/a, target: d:/b.txt
await workspace.saveAs(docUri);
return;
}

const workspaceFolder = workspace.getWorkspaceFolder(docUri);
if (!workspaceFolder) {
// Let the user choose the save location
// Otherwise, we would have to do too much guessing
await workspace.saveAs(docUri);
return;
}
const saveUri = Uri.joinPath(workspaceFolder.uri, relativePath);
if ((await fileExists(saveUri)) && !bang) {
// When will this be reached?
// In remote development with Nvim running locally
// Nvim can't detect if the file exists, so the user might not be able to use "!"
const ret = await window.showErrorMessage(`File exists (add ! to override): ${saveUri.fsPath}`, "Override");
if (ret !== "Override") {
return;
}
}

logger.debug(`Saving ${docUri} to ${saveUri}`);

const text = document.getText();
const bytes = new TextEncoder().encode(text);
await workspace.fs.writeFile(saveUri, bytes);
const doc = await workspace.openTextDocument(saveUri);
await window.showTextDocument(doc);
}

// #region Sync layout

private onEditorLayoutChanged = async () => {
Expand Down Expand Up @@ -607,56 +706,41 @@ export class BufferManager implements Disposable {
*/
private async initBufferForDocument(document: TextDocument, buffer: Buffer, editor?: TextEditor): Promise<void> {
const bufId = buffer.id;
const { uri: docUri } = document;
logger.log(docUri, LogLevel.Debug, `Init buffer for ${bufId}, doc: ${docUri}`);
logger.log(document.uri, LogLevel.Debug, `Init buffer for ${bufId}, doc: ${document.uri}`);

const eol = document.eol === EndOfLine.LF ? "\n" : "\r\n";
const lines = document.getText().split(eol);
// We don't care about the name of the buffer if it's not a file
const bufname =
docUri.scheme === "file"
? config.useWsl
? await actions.lua("wslpath", docUri.fsPath)
: docUri.fsPath
: docUri.toString();

await this.client.lua(
`
local bufId, lines, vscode_editor_options, docUri, docUriJson, bufname, isExternalDoc = ...
vim.api.nvim_buf_set_lines(bufId, 0, -1, false, lines)
-- set vscode controlled flag so we can check it neovim
vim.api.nvim_buf_set_var(bufId, "vscode_controlled", true)
-- In vscode same document can have different insertSpaces/tabSize settings per editor
-- however in neovim it's per buffer. We make assumption here that these settings are same for all editors
vim.api.nvim_buf_set_var(bufId, "vscode_editor_options", vscode_editor_options)
vim.api.nvim_buf_set_var(bufId, "vscode_uri", docUri)
vim.api.nvim_buf_set_var(bufId, "vscode_uri_data", docUriJson)
vim.api.nvim_buf_set_name(bufId, bufname)
vim.api.nvim_buf_set_option(bufId, "modifiable", not isExternalDoc)
-- force nofile, just in case if the buffer was created externally
vim.api.nvim_buf_set_option(bufId, "buftype", "nofile")
vim.api.nvim_buf_set_option(bufId, "buflisted", true)
`,
[
bufId,
lines,
makeEditorOptionsVariable(editor?.options),
docUri.toString(),
docUri.toJSON(),
bufname,
this.isExternalTextDocument(document),
],
);
const bufname = await this.bufnameForTextDocument(document);

await actions.lua("init_document_buffer", {
buf: bufId,
bufname: bufname,
lines: lines,
uri: document.uri.toString(),
uri_data: document.uri.toJSON(),
editor_options: makeEditorOptionsVariable(editor?.options),
modifiable: !this.isExternalTextDocument(document),
modified: document.isDirty,
});

// Looks like need to be in separate request
if (!this.isExternalTextDocument(document)) {
await this.client.callFunction("VSCodeClearUndo", bufId);
await actions.lua("clear_undo", bufId);
}
this.onBufferInit?.(bufId, document);
buffer.listen("lines", this.receivedBufferEvent);
actions.fireNvimEvent("document_buffer_init", bufId);
}

private async bufnameForTextDocument(doc: TextDocument): Promise<string> {
const uri = doc.uri;
if (uri.scheme === "file") {
return config.useWsl ? actions.lua<string>("wslpath", uri.fsPath) : uri.fsPath;
}
// We don't care about the name of the buffer if it's not a file
return uri.toString();
}

/**
* Create new neovim window
*/
Expand Down
Loading

0 comments on commit 8606eb1

Please sign in to comment.