Skip to content

Commit

Permalink
feat: support bulk renaming files in nightly yazi (opt-in)
Browse files Browse the repository at this point in the history
This change is optional, and by default is not enabled for users.

To opt in, you need to do the following:
- install the latest `yazi` from source
- install the latest `ya` from source. `ya` is the standalone command
  line tool for yazi. You can read
  <sxyazi/yazi#914> for more information.
- set the new config option `use_ya_for_events_reading = true` (it's
  `false` by default for now)
- run `:checkhealth yazi` to verify that `ya` is found and that the
  healthcheck passes

Bulk renaming in yazi is done by
- selecting multiple files in yazi
- pressing `r` to rename the files. This will open `$EDITOR`, typically
  Neovim, inside yazi.
- The nested Neovim will display the selected file names, one per line.
  The user can then edit the names as needed.
- save and quit the nested Neovim instance. This will rename the files
  in yazi.

<https://yazi-rs.github.io/docs/configuration/keymap/#manager.rename>

---

Technical details for developers:

- in the future, once the next release of yazi is out, yazi.nvim may
  automatically use `ya` for reading events. This would then be the
  default behavior. `ya` may enable more advanced features in the
  future.
- `yazi` is now built from source in continuous integration
- the integration testing system, although perhaps innovative, is still
  taking shape. I believe in time it will stabilize, but I don't want to
  cement the design too much until the best structure becomes evident in
  time.

Closes #135
  • Loading branch information
mikavilpas committed Jul 7, 2024
1 parent a2f7832 commit 3caaf34
Show file tree
Hide file tree
Showing 41 changed files with 1,354 additions and 307 deletions.
41 changes: 24 additions & 17 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,44 @@ jobs:

steps:
- uses: actions/checkout@v4
- name: Set up yazi
- name: Set up ripgrep
# it's a telescope dependency
run: |
# Install yazi
test -d _yazi || {
mkdir -p _yazi
wget "https://github.com/sxyazi/yazi/releases/download/v0.2.5/yazi-x86_64-unknown-linux-gnu.zip" --output-document yazi.zip
unzip yazi.zip -d _yazi
which rg || {
sudo apt-get install ripgrep
}
echo "Current _yazi/ contents"
ls -R _yazi
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path
echo "${PWD}/_yazi/yazi-x86_64-unknown-linux-gnu/" >> $GITHUB_PATH
- name: Compile and install `yazi-fm` from source
uses: baptiste0928/cargo-install@v3
with:
# yazi-fm is the `yazi` executable
crate: yazi-fm
git: https://github.com/sxyazi/yazi
# feat: ownership linemode (#1238)
# https://github.com/sxyazi/yazi/commit/11547eefe0346006a1a82455577784a34d67c9b7
commit: 11547eefe0346006a1a82455577784a34d67c9b7

- name: Compile and install yazi from source
uses: baptiste0928/cargo-install@v3
with:
# yazi-cli is the `ya` command line interface
crate: yazi-cli
git: https://github.com/sxyazi/yazi
# feat: ownership linemode (#1238)
# https://github.com/sxyazi/yazi/commit/11547eefe0346006a1a82455577784a34d67c9b7
commit: 11547eefe0346006a1a82455577784a34d67c9b7

- name: Run tests
uses: nvim-neorocks/nvim-busted-action@v1
with:
nvim_version: ${{ matrix.neovim_version }}
luarocks_version: "3.11.1"
- name: Set up ripgrep
run: |
which rg || {
sudo apt-get install ripgrep
}

# Install npm dependencies, cache them correctly
# and run all Cypress tests
- name: Cypress run
uses: cypress-io/[email protected]
with:
command: npm run cy:run

- uses: actions/upload-artifact@v4
# add the line below to store screenshots only on failures
# if: failure()
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ integration-tests/server/build/server.js
integration-tests/pid.txt
integration-tests/server/build
*.pem
integration-tests/test-environment/testdirs
3 changes: 3 additions & 0 deletions .luarc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"diagnostics.globals": ["finally"]
}
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ init:

lint:
selene ./lua/ ./spec/
@if grep -r -e "#focus" --include \*.lua .; then \
@if grep -r -e "#focus" --include \*.lua ./spec/; then \
echo "\n"; \
echo "Error: ${COLOR_GREEN}#focus${COLOR_RESET} tags found in the codebase.\n"; \
echo "Please remove them to prevent issues with not accidentally running all tests."; \
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ You can optionally configure yazi.nvim by setting any of the options below.
-- }
open_for_directories = false,

-- an upcoming optional feature. See
-- https://github.com/mikavilpas/yazi.nvim/pull/152
use_ya_for_events_reading = false,

-- the floating window scaling factor. 1 means 100%, 0.9 means 90%, etc.
floating_window_scaling_factor = 0.9,

Expand Down
22 changes: 17 additions & 5 deletions integration-tests/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { FitAddon } from "@xterm/addon-fit"
import { Terminal } from "@xterm/xterm"
import io from "socket.io-client"
import type {
StartAppMessage,
StartNeovimMessage,
StdinMessage,
StdoutMessage,
} from "../server/server"
import "./startAppGlobalType"
import type { StartAppMessageArguments } from "./startAppGlobalType"
import type {
StartNeovimArguments,
StartNeovimServerArguments,
} from "./testEnvironmentTypes"

const app = document.querySelector<HTMLDivElement>("#app")
if (!app) {
Expand Down Expand Up @@ -73,8 +75,18 @@ socket.on("disconnect", (reason) => {
console.log("disconnected: ", reason)
})

window.startApp = function startApp(args: StartAppMessageArguments) {
socket.emit("startApp" satisfies StartAppMessage, args)
window.startNeovim = async function startApp(
directory: string,
startArgs?: StartNeovimArguments,
) {
await socket.emitWithAck(
"startNeovim" satisfies StartNeovimMessage,
{
directory,
filename: startArgs?.filename ?? "initial-file.txt",
startupScriptModifications: startArgs?.startupScriptModifications,
} satisfies StartNeovimServerArguments,
)
}

socket.on(
Expand Down
12 changes: 0 additions & 12 deletions integration-tests/client/startAppGlobalType.ts

This file was deleted.

65 changes: 65 additions & 0 deletions integration-tests/client/testEnvironmentTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/** The arguments given from the tests to send to the server */
export type StartNeovimArguments = {
filename?: TestDirectoryFile | "."
startupScriptModifications?: StartupScriptModification[]
}

/** The arguments given to the server */
export type StartNeovimServerArguments = {
directory: string
} & StartNeovimArguments

export type StartupScriptModification =
"modify_yazi_config_to_use_ya_as_event_reader.lua"

declare global {
interface Window {
startNeovim(
directory: string,
startArguments?: StartNeovimArguments,
): Promise<void>
}
}

export type FileEntry = {
/** The name of the file and its extension.
* @example "file.txt"
*/
name: string

/** The name of the file without its extension.
* @example "file"
*/
stem: string

/** The extension of the file.
* @example ".txt"
*/
extension: string
}

/** Describes the contents of the test directory, which is a blueprint for
* files and directories. Tests can create a unique, safe environment for
* interacting with the contents of such a directory.
*
* Having strong typing for the test directory contents ensures that tests can
* be written with confidence that the files and directories they expect are
* actually found. Otherwise the tests are brittle and can break easily.
*/
export type TestDirectory = {
/** The path to the unique test directory itself (the root). */
rootPath: string

contents: {
["initial-file.txt"]: FileEntry
["test.lua"]: FileEntry
["file.txt"]: FileEntry
["subdirectory/sub.txt"]: FileEntry
["routes/posts.$postId/route.tsx"]: FileEntry
["routes/posts.$postId/adjacent-file.tsx"]: FileEntry
}
}

type TestDirectoryFile = keyof TestDirectory["contents"]

export {}
134 changes: 132 additions & 2 deletions integration-tests/cypress.config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,139 @@
import assert from "assert"
import { execSync, Serializable } from "child_process"
import { defineConfig } from "cypress"
import { constants } from "fs"
import { access, mkdir, mkdtemp, readdir, readFile, rm } from "fs/promises"
import path from "path"
import { fileURLToPath } from "url"
import type { TestDirectory } from "./cypress/support/commands"

const __dirname = fileURLToPath(new URL(".", import.meta.resolve(".")))

// const file = "./test-environment/.repro/state/nvim/yazi.log"
const yaziLogFile = path.join(
__dirname,
"test-environment",
".repro",
"state",
"nvim",
"yazi.log",
)

console.log(`yaziLogFile: ${yaziLogFile}`)

export default defineConfig({
e2e: {
setupNodeEvents(on, config) {
// implement node event listeners here
setupNodeEvents(on, _config) {
on("after:browser:launch", async (): Promise<void> => {
// delete everything under the ./test-environment/testdirs/ directory
const testdirs = path.join(__dirname, "test-environment", "testdirs")
await mkdir(testdirs, { recursive: true })
const files = await readdir(testdirs)

console.log("Cleaning up testdirs directory...")

for (const file of files) {
const testdir = path.join(testdirs, file)
console.log(`Removing ${testdir}`)
await rm(testdir, { recursive: true })
}
})

on("task", {
async removeYaziLog() {
try {
await rm(yaziLogFile)
} catch (err) {
if (err.code !== "ENOENT") {
console.error(err)
}
}
return null // something must be returned
},
async showYaziLog() {
try {
const log = await readFile(yaziLogFile, "utf-8")
console.log(`${yaziLogFile}`, log.split("\n"))
return null
} catch (err) {
console.error(err)
return null // something must be returned
}
},
async createTempDir(): Promise<TestDirectory> {
try {
const dir = await createUniqueDirectory()

const directory: TestDirectory = {
rootPath: dir,
contents: {
"initial-file.txt": {
name: "initial-file.txt",
stem: "initial-file",
extension: ".txt",
},
"test.lua": {
name: "test.lua",
stem: "test",
extension: ".lua",
},
"file.txt": {
name: "file.txt",
stem: "file",
extension: ".txt",
},
"subdirectory/sub.txt": {
name: "sub.txt",
stem: "sub",
extension: ".txt",
},
"routes/posts.$postId/adjacent-file.tsx": {
name: "adjacent-file.tsx",
stem: "adjacent-file",
extension: ".tsx",
},
"routes/posts.$postId/route.tsx": {
name: "route.tsx",
stem: "route",
extension: ".tsx",
},
},
}
directory satisfies Serializable // required by cypress

execSync(`cp ./test-environment/initial-file.txt ${dir}/`)
execSync(`cp ./test-environment/file.txt ${dir}/`)
execSync(`cp ./test-environment/test-setup.lua ${dir}/test.lua`)
execSync(`cp -r ./test-environment/subdirectory ${dir}/`)
execSync(`cp -r ./test-environment/config-modifications/ ${dir}/`)
execSync(`cp -r ./test-environment/routes ${dir}/`)
console.log(`Created test directory at ${dir}`)

return directory
} catch (err) {
console.error(err)
throw err
}
},
})
},
retries: {
runMode: 2,
openMode: 0,
},
},
})

async function createUniqueDirectory(): Promise<string> {
const __dirname = fileURLToPath(new URL(".", import.meta.resolve(".")))
const testdirs = path.join(__dirname, "test-environment", "testdirs")
try {
await access(testdirs, constants.F_OK)
} catch {
await mkdir(testdirs)
}
const dir = await mkdtemp(path.join(testdirs, "dir-"))
assert(typeof dir === "string")

return dir
}
8 changes: 6 additions & 2 deletions integration-tests/cypress/e2e/healthcheck.cy.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { startNeovimWithYa } from "./using-ya-to-read-events/startNeovimWithYa"

describe("the healthcheck", () => {
it("can run the :healthcheck for yazi.nvim", () => {
cy.visit("http://localhost:5173")
cy.startNeovim()
startNeovimWithYa()

// wait until text on the start screen is visible
cy.contains("If you see this text, Neovim is ready!")

cy.typeIntoTerminal(":checkhealth yazi{enter}")

// the `yazi` application should be found successfully
// the `yazi` and `ya` applications should be found successfully
cy.contains("Found yazi version 0.2.5")
cy.contains("Found ya version 0.2.5")
cy.contains("OK yazi")
})
})
Loading

0 comments on commit 3caaf34

Please sign in to comment.