duplicate text editor

This commit is contained in:
2023-04-09 11:55:17 -06:00
parent 0e8d4dd26a
commit 7ce569f533
13 changed files with 2358 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
bin/
*.vsix
node_modules/

View File

@@ -0,0 +1,19 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"stopOnEntry": false,
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/*.js"
]
}
]
}

View File

@@ -0,0 +1,13 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "hxml",
"file": "build.hxml",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@@ -0,0 +1,6 @@
.vscode
bin/*.map
src
build.hxml
haxelib.json
test.sh

View File

@@ -0,0 +1,20 @@
# duplicate-text-editor
Sometimes you have to edit text that will be duplicated in multiple files. For example,
when writing a tutorial which includes code snippets from an example project.
This VSCode extension lets you edit a text snippet simultaneously in every file where it occurs.
Like [LinkedEditingRanges](https://code.visualstudio.com/api/references/vscode-api#LinkedEditingRanges) but across multiple files and with no safety guarantees.
## Features
Attempts to be smart about cases where the snippet is indented differently in different files.
## Extension Settings
## Known Issues
Can't be used to de-indent a snippet.
## Release Notes

View File

@@ -0,0 +1,10 @@
-lib kiss-vscode-api
-lib kiss
-cp src
-js bin/extension.js
-dce full
-D analyzer-optimize
-D js-es=6
-debug
Main
-cmd npx vsce package

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
{
"main": "bin/extension.js",
"name": "duplicate-text-editor",
"description": "",
"repository": {
"url": "",
"type:": "git"
},
"homepage": "",
"categories": [],
"extensionPack": [],
"publisher": "",
"contributes": {
"keybindings": [
{
"command": "duplicate-text-editor.startEditingSelection",
"mac": "Cmd+; Cmd+d",
"key": "Ctrl+; Ctrl+d"
}
],
"commands": [
{
"title": "duplicate-text-editor: Edit the currently selected text wherever it occurs in your workspace",
"command": "duplicate-text-editor.startEditingSelection"
}
]
},
"engines": {
"vscode": "^1.4.0"
},
"devDependencies": {
"vsce": "^2.15.0"
},
"version": "0.0.0",
"activationEvents": [],
"displayName": ""
}

View File

@@ -0,0 +1,10 @@
import kiss.Prelude;
import kiss.List;
import haxe.Constraints;
import vscode.*;
using StringTools;
@:build(kiss.Kiss.build())
class Main {}

View File

@@ -0,0 +1,97 @@
(loadFrom "kiss-vscode-api" "src/Util.kiss")
(loadFrom "kiss-vscode-api" "src/KissUtil.kiss")
(function :kiss.List<Dynamic>->Bool matchesLine [ltrimmedLine]
->[:Array<Dynamic> lineAndIndex]
(let [[:Int index :String line] lineAndIndex]
(= (line.ltrim) ltrimmedLine)))
(function :Null<Int> indentationDeltasMatch [:Array<String> indentation1 :Array<String>> indentation2]
"If indentation follows the same pattern, return the first lowest level of the second one"
(let [lengths (for list [indentation1 indentation2] (for line list line.length))
deltaLists (for list lengths ~(for [from to] (pairs list) (- to from)))
:Array<Array<Int>> zipped (apply zip deltaLists)]
(doFor [a b] zipped
(unless (= a b)
(return null)))
(apply min (second lengths))))
@(:expose "activate")
(function activate [:ExtensionContext context]
(printThroughInfoMessage)
// Add your extension's commands here with (defCommand <...>):
// (defCommand context yourExt.exampleCommand "An example command for your extension" "C-; C-1" [] (doSomething))
(defCommand context startEditingSelection "Edit the currently selected text wherever it occurs in your workspace" "C-; C-d" []
(assert activeTextEditor)
(assert activeTextEditor.selection)
(assert Vscode.workspace.workspaceFolders)
(let [start (activeTextEditor.selection.start.with (object line null character 0))
end (.with (activeTextEditor.selection.end.translate (object lineDelta 1 characterDelta null)) (object line null character 0))
:String originalText (activeTextEditor.document.getText (new Range start end))
originalLines (originalText.split "\n")
_empty (originalLines.pop)
ltrimmedLines (for line originalLines (line.ltrim))
originalIndentation (for [original ltrimmed] (zip originalLines ltrimmedLines) (original.substr 0 (- original.length ltrimmed.length)))
// Contains [file indentation index firstLine]
:Array<Array<Dynamic>> placesToEdit []]
(doFor folder Vscode.workspace.workspaceFolders
(walkDirectory "" folder.uri.fsPath
->file
(let [lines (.split (sys.io.File.getContent file) "\n")
enumeratedLines (enumerate lines)
linesMatchingFirstLine
(filter enumeratedLines
(matchesLine (first ltrimmedLines)))]
(localFunction matchesBlock [:Array<Dynamic> firstLineAndIndex]
(let [indentation []
linesToCheck (enumerate (lines.slice (first firstLineAndIndex) (+ (first firstLineAndIndex) originalLines.length)))]
(unless (= linesToCheck.length originalLines.length)
(return null))
(doFor [index :String line] linesToCheck
(unless ((matchesLine (nth ltrimmedLines index)) (nth linesToCheck index))
(return null))
(indentation.push (line.substr 0 (- line.length .length (line.ltrim)))))
indentation))
(doFor lineAndIndex linesMatchingFirstLine
(whenLet [indentation (matchesBlock lineAndIndex)]
(lineAndIndex.unshift indentation)
(lineAndIndex.unshift file)
(placesToEdit.push lineAndIndex))))
->folder (case (haxe.io.Path.withoutDirectory folder)
((or "node_modules" "bin") true) // Don't recurse into node_modules or generated files
// TODO make a list of folders to ignore part of extension configuration
(otherwise false))))
(if placesToEdit
{
(doFor place placesToEdit
(assert (indentationDeltasMatch originalIndentation (second place))))
(print "Editing this text in ${placesToEdit.length} places.")
(awaitLet [editedText (inputEditor "duplicateEditor.$(haxe.io.Path.extension activeTextEditor.document.fileName)" "Apply your edits" originalText)
&sync editedLines (editedText.split "\n")
&sync editedLtrimmedLines (for line editedLines (line.ltrim))
&sync editedIndentation (for [edited ltrimmed] (zip editedLines editedLtrimmedLines) (edited.substr 0 (- edited.length ltrimmed.length)))
&sync indentationChar (->{(doFor :String whitespace editedIndentation (when whitespace (return (whitespace.charAt 0)))) ""})
&sync originalMinIndentation (apply min (for line originalIndentation line.length))
&sync editedMinIndentation (apply min (for line editedIndentation line.length))]
// Reverse placesToEdit so we edit files from end to start, preserving line number validity
(doFor [file indentation blockLineNumber line] (reverse placesToEdit)
(let [minIndentation (indentationDeltasMatch originalIndentation indentation)
fileLines (.split (sys.io.File.getContent file) "\n")]
// TODO do this by opening all the files and using reversible edit operations
(fileLines.splice blockLineNumber originalLines.length)
(doFor [index line] (enumerate editedLtrimmedLines)
(fileLines.insert (+ blockLineNumber index) "$(* minIndentation indentationChar)$(* (- .length (nth editedIndentation index) editedMinIndentation) indentationChar)${line}"))
(sys.io.File.saveContent file (fileLines.join "\n")))))
null
}
(print "The selected text is not duplicated anywhere in your workspace folders (unless you are filtering it by accident!)."))
))
// Add your extension's configuration here with (defConfiguration <...>):
// (defConfiguration
// :Bool configBool
// (object
// default false)
// :String configString
// (object
// default ""))
)

View File

@@ -0,0 +1,3 @@
#! /bin/bash
haxe build.hxml

View File

@@ -0,0 +1,4 @@
Contains duplicated text.
A block of duplicated text.
Containing 3 separate lines.

View File

@@ -0,0 +1,11 @@
Contains duplicated text.
A block of duplicated text.
Containing 3 separate lines.
Contains duplicated text.
A block of duplicated text.
Containing 3 separate lines.