opencode-fastedit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/index.ts +130 -0
  2. package/package.json +31 -0
  3. package/tsconfig.json +13 -0
package/index.ts ADDED
@@ -0,0 +1,130 @@
1
+ import { type Plugin, tool } from "@opencode-ai/plugin";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { createTwoFilesPatch } from "diff";
5
+
6
+ function normalizeLineEndings(text: string): string {
7
+ return text.replaceAll("\r\n", "\n");
8
+ }
9
+
10
+ const FastEditPlugin: Plugin = async ({ directory, client }) => {
11
+ return {
12
+ tool: {
13
+ fastedit: tool({
14
+ description: `Replace a range of lines in a file with new code without needing to specify the old content.
15
+
16
+ This tool efficiently replaces or deletes lines by line number instead of requiring you to type out the exact text being replaced. This is useful when:
17
+ - You want to replace multiple lines at once
18
+ - The old code is long and tedious to type
19
+ - You know the line numbers you want to modify
20
+
21
+ BEHAVIOR:
22
+ - Deletes all lines from start_line to end_line (inclusive)
23
+ - Inserts new_code at the start_line position
24
+ - If new_code is an empty string, the lines are simply deleted (pure deletion)
25
+ - If new_code contains multiple lines, they are all inserted starting at start_line
26
+
27
+ LINE NUMBERING:
28
+ - Uses 1-indexed line numbers (line 1 is the first line of the file)
29
+ - This matches the line numbers shown by the "view" tool
30
+
31
+ EXAMPLES:
32
+ - To replace lines 5-10 with new code: start_line=5, end_line=10, new_code="..."
33
+ - To delete lines 5-10: start_line=5, end_line=10, new_code=""
34
+ - To insert new code at line 5 (pushing existing line 5 and beyond down): use end_line=4 to "replace" a zero-length range
35
+
36
+ RETURN VALUE:
37
+ - Returns a success message with the file path and line range that was modified`,
38
+ args: {
39
+ file_path: tool.schema.string().describe("The absolute path to the file to modify"),
40
+ start_line: tool.schema
41
+ .number()
42
+ .min(1)
43
+ .describe("The starting line number (1-indexed) of the range to replace. Line 1 is the first line of the file."),
44
+ end_line: tool.schema
45
+ .number()
46
+ .min(1)
47
+ .describe("The ending line number (inclusive) of the range to replace. Must be >= start_line."),
48
+ new_code: tool.schema
49
+ .string()
50
+ .describe("The new code to insert at the start_line position. Can be empty string to simply delete the line range."),
51
+ },
52
+ async execute(args, context) {
53
+ const { file_path, start_line, end_line, new_code } = args;
54
+
55
+ const resolvedPath = path.isAbsolute(file_path)
56
+ ? file_path
57
+ : path.join(context.worktree, file_path)
58
+
59
+ if (!fs.existsSync(resolvedPath)) {
60
+ throw new Error(`File does not exist: ${resolvedPath}`)
61
+ }
62
+
63
+ const stat = fs.statSync(resolvedPath)
64
+ if (stat.isDirectory()) {
65
+ throw new Error(`Path is a directory, not a file: ${resolvedPath}`)
66
+ }
67
+
68
+ if (start_line > end_line) {
69
+ throw new Error(`start_line (${start_line}) must be <= end_line (${end_line})`)
70
+ }
71
+
72
+ const contentOld = normalizeLineEndings(fs.readFileSync(resolvedPath, "utf-8"))
73
+ const lines = contentOld.split("\n")
74
+
75
+ if (start_line > lines.length) {
76
+ throw new Error(
77
+ `start_line (${start_line}) exceeds file length (${lines.length} lines). Maximum valid line number is ${lines.length}.`
78
+ )
79
+ }
80
+
81
+ if (end_line > lines.length) {
82
+ throw new Error(
83
+ `end_line (${end_line}) exceeds file length (${lines.length} lines). Maximum valid line number is ${lines.length}.`
84
+ )
85
+ }
86
+
87
+ const beforeLines = lines.slice(0, start_line - 1)
88
+ const afterLines = lines.slice(end_line)
89
+
90
+ const contentNew = normalizeLineEndings([...beforeLines, new_code, ...afterLines].join("\n"))
91
+
92
+ const diff = createTwoFilesPatch(resolvedPath, resolvedPath, contentOld, contentNew)
93
+
94
+ fs.writeFileSync(resolvedPath, contentNew, "utf-8")
95
+
96
+ const deletedCount = end_line - start_line + 1
97
+ const insertedLines = new_code ? new_code.split("\n").length : 0
98
+ const added = insertedLines
99
+ const removed = deletedCount
100
+
101
+ return `FastEdit applied to ${resolvedPath}
102
+
103
+ +${added} -${removed} lines
104
+
105
+ \`\`\`diff
106
+ ${diff}
107
+ \`\`\``
108
+ },
109
+ }),
110
+ },
111
+
112
+ "tool.execute.after": async (input, output) => {
113
+ if (input.tool === "fastedit") {
114
+ const fileMatch = output.output.match(/FastEdit applied to (.+?)\n/);
115
+ const statsMatch = output.output.match(/\+(\d+) -(\d+) lines/);
116
+
117
+ if (fileMatch && statsMatch) {
118
+ output.title = `FastEdit: ${path.basename(fileMatch[1])} +${statsMatch[1]}/-${statsMatch[2]}`;
119
+ }
120
+
121
+ output.metadata = {
122
+ ...output.metadata,
123
+ provider: "fastedit",
124
+ };
125
+ }
126
+ },
127
+ };
128
+ };
129
+
130
+ export default FastEditPlugin;
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "opencode-fastedit",
3
+ "version": "1.0.0",
4
+ "description": "OpenCode plugin for fast line-based editing by line numbers",
5
+ "type": "module",
6
+ "main": "index.ts",
7
+ "scripts": {
8
+ "test": "bun test",
9
+ "typecheck": "tsc --noEmit"
10
+ },
11
+ "dependencies": {
12
+ "@opencode-ai/plugin": "latest",
13
+ "diff": "^8.0.2"
14
+ },
15
+ "devDependencies": {
16
+ "@types/diff": "^7.0.2",
17
+ "bun-types": "latest",
18
+ "typescript": "^5.0.0"
19
+ },
20
+ "keywords": [
21
+ "opencode",
22
+ "plugin",
23
+ "fastedit"
24
+ ],
25
+ "author": "mudaaaa",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/mudaaaa/opencode-fastedit.git"
29
+ },
30
+ "license": "MIT"
31
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "esModuleInterop": true,
7
+ "strict": true,
8
+ "skipLibCheck": true,
9
+ "declaration": true,
10
+ "outDir": "./dist"
11
+ },
12
+ "include": ["*.ts"]
13
+ }