├── icon.png
├── .gitignore
├── .vscodeignore
├── .vscode
├── extensions.json
├── tasks.json
├── settings.json
└── launch.json
├── src
├── extension.ts
├── settings.ts
├── app.ts
├── diff.ts
├── ipc.ts
├── command-handler.ts
└── diff_match_patch.js
├── tsconfig.json
├── webpack.config.js
├── LICENSE
├── package.json
├── README.md
└── yarn.lock
/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/serenadeai/code/HEAD/icon.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | out
3 | node_modules
4 | .vscode-test/
5 | *.vsix
6 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/
2 | .vscode-test/
3 | out/
4 | src/
5 | .gitignore
6 | tsconfig.json
7 | tslint.json
8 | node_modules
9 | webpack.config.js
10 | **/*.map
11 | **/*.ts
12 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // See http://go.microsoft.com/fwlink/?LinkId=827846
3 | // for the documentation about the extensions.json format
4 | "recommendations": [
5 | "ms-vscode.vscode-typescript-tslint-plugin"
6 | ]
7 | }
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import App from "./app";
3 |
4 | let app: App | null = null;
5 |
6 | export function activate(_context: vscode.ExtensionContext) {
7 | app = new App();
8 | app.start();
9 | }
10 |
11 | export function deactivate() {}
12 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | // See https://go.microsoft.com/fwlink/?LinkId=733558
2 | // for the documentation about the tasks.json format
3 | {
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "type": "npm",
8 | "script": "watch",
9 | "problemMatcher": "$tsc-watch",
10 | "isBackground": true,
11 | "presentation": {
12 | "reveal": "never"
13 | },
14 | "group": {
15 | "kind": "build",
16 | "isDefault": true
17 | }
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "*": ["types/*"]
6 | },
7 | "module": "commonjs",
8 | "target": "es6",
9 | "outDir": "out",
10 | "lib": ["es6", "dom"],
11 | "sourceMap": true,
12 | "rootDir": "src",
13 | "strict": true,
14 | "noImplicitReturns": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "noUnusedParameters": true,
17 | "preserveSymlinks": true,
18 | "esModuleInterop": true
19 | },
20 | "exclude": ["node_modules", ".vscode-test"]
21 | }
22 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | // Place your settings in this file to overwrite default and user settings.
2 | {
3 | "files.exclude": {
4 | "out": false,
5 | "**/.classpath": true,
6 | "**/.project": true,
7 | "**/.settings": true,
8 | "**/.factorypath": true
9 | },
10 | "search.exclude": {
11 | "out": true // set this to false to include "out" folder in search results
12 | },
13 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts
14 | "typescript.tsc.autoDetect": "off"
15 | }
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const path = require("path");
4 |
5 | module.exports = [
6 | {
7 | name: "extension",
8 | target: "node",
9 | entry: {
10 | extension: "./src/extension.ts"
11 | },
12 | output: {
13 | path: path.resolve(__dirname, "build"),
14 | filename: "[name].js",
15 | libraryTarget: "commonjs2",
16 | devtoolModuleFilenameTemplate: "../[resource-path]"
17 | },
18 | devtool: "source-map",
19 | externals: {
20 | vscode: "commonjs vscode"
21 | },
22 | resolve: {
23 | extensions: [".tsx", ".ts", ".js"],
24 | symlinks: false
25 | },
26 | module: {
27 | rules: [
28 | {
29 | test: /\.tsx?$/,
30 | use: "ts-loader",
31 | exclude: /node_modules/
32 | }
33 | ]
34 | }
35 | }
36 | ];
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022 Serenade Labs, Inc.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that compiles the extension and then opens it inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [{
8 | "name": "Run Extension",
9 | "type": "extensionHost",
10 | "request": "launch",
11 | "runtimeExecutable": "${execPath}",
12 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/out/**/*.js"
17 | ],
18 | "preLaunchTask": "npm: watch"
19 | },
20 | {
21 | "name": "Extension Tests",
22 | "type": "extensionHost",
23 | "request": "launch",
24 | "runtimeExecutable": "${execPath}",
25 | "args": [
26 | "--extensionDevelopmentPath=${workspaceFolder}",
27 | "--extensionTestsPath=${workspaceFolder}/out/test"
28 | ],
29 | "outFiles": [
30 | "${workspaceFolder}/out/test/**/*.js"
31 | ],
32 | "preLaunchTask": "npm: watch"
33 | }
34 | ]
35 | }
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "serenade",
3 | "description": "Code with voice. Learn more at https://serenade.ai.",
4 | "displayName": "Serenade",
5 | "license": "MIT",
6 | "publisher": "serenade",
7 | "repository": "https://github.com/serenadeai/code",
8 | "version": "1.5.3",
9 | "extensionKind": "ui",
10 | "icon": "icon.png",
11 | "engines": {
12 | "vscode": "^1.44.0"
13 | },
14 | "categories": [
15 | "Other"
16 | ],
17 | "activationEvents": [
18 | "*"
19 | ],
20 | "main": "./build/extension.js",
21 | "scripts": {
22 | "vscode:prepublish": "webpack --mode production",
23 | "build": "webpack --mode production",
24 | "dev": "webpack --mode development --watch",
25 | "watch": "exit 0"
26 | },
27 | "dependencies": {
28 | "bufferutil": "^4.0.1",
29 | "gitignore-globs": "^0.1.1",
30 | "mkdirp": "0.5.1",
31 | "utf-8-validate": "^5.0.2",
32 | "uuid": "^7.0.2",
33 | "ws": "^7.2.3"
34 | },
35 | "devDependencies": {
36 | "@types/mkdirp": "^0.5.2",
37 | "@types/node": "^10.14.12",
38 | "@types/uuid": "^7.0.2",
39 | "@types/vscode": "1.44",
40 | "@types/ws": "^7.2.3",
41 | "ts-loader": "^9.2.6",
42 | "typescript": "^4.6.2",
43 | "webpack": "^5.69.1",
44 | "webpack-cli": "^4.9.2"
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Serenade for Visual Studio Code
4 |
5 | To use Serenade for Visual Studio Code, you'll also need the Serenade app, which is freely available at [https://serenade.ai](https://serenade.ai).
6 |
7 | ## Code with voice
8 |
9 | Serenade is the fastest and easiest way to write code with natural speech. Give your hands a break without missing a beat.
10 |
11 | Edit code, run terminal commands, and write documentation entirely with voice. Whether you have an injury or you’re looking to prevent one, Serenade can help you be just as productive without lifting a finger. Use voice alongside your existing workflow, or abandon your keyboard entirely.
12 |
13 | Learn more at [https://serenade.ai](https://serenade.ai).
14 |
15 | [](https://serenade.ai/)
16 |
17 | ## Installation
18 |
19 | To use Serenade with VS Code, you'll also need the Serenade app, available for download [here](https://serenade.ai/download). Once Serenade is installed, restart VS Code, and you'll be able to start using voice commands.
20 |
21 | ## Getting Started
22 |
23 | Check out the [Serenade documentation](https://serenade.ai/docs) to learn how to set up and start using Serenade.
24 |
--------------------------------------------------------------------------------
/src/settings.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as mkdirp from "mkdirp";
3 | import * as os from "os";
4 |
5 | export default class Settings {
6 | private systemData: any = {};
7 | private systemDefaults: any = {};
8 | private userData: any = {};
9 | private userDefaults: any = {};
10 |
11 | private createIfNotExists(file: string) {
12 | mkdirp.sync(this.path());
13 | if (!fs.existsSync(file)) {
14 | fs.closeSync(fs.openSync(file, "w"));
15 | }
16 | }
17 |
18 | private dataForFile(file: string): any {
19 | if (file == "user") {
20 | return this.userData;
21 | } else if (file == "system") {
22 | return this.systemData;
23 | }
24 | }
25 |
26 | private defaultsForFile(file: string): any {
27 | if (file == "user") {
28 | return this.userDefaults;
29 | } else if (file == "system") {
30 | return this.systemDefaults;
31 | }
32 | }
33 |
34 | private get(file: string, key: string): any {
35 | this.load();
36 | let data = this.dataForFile(file);
37 | if (data[key] === undefined) {
38 | return this.defaultsForFile(file)[key];
39 | }
40 |
41 | return data[key];
42 | }
43 |
44 | private load() {
45 | this.systemData = {};
46 | this.userData = {};
47 |
48 | try {
49 | this.systemData = JSON.parse(fs.readFileSync(this.systemFile()).toString());
50 | } catch (e) {
51 | this.systemData = {};
52 | }
53 |
54 | try {
55 | this.userData = JSON.parse(fs.readFileSync(this.userFile()).toString());
56 | } catch (e) {
57 | this.userData = {};
58 | }
59 | }
60 |
61 | private save() {
62 | this.createIfNotExists(this.systemFile());
63 | this.createIfNotExists(this.userFile());
64 |
65 | fs.writeFileSync(this.systemFile(), JSON.stringify(this.systemData, null, 2));
66 | fs.writeFileSync(this.userFile(), JSON.stringify(this.userData, null, 2));
67 | }
68 |
69 | private set(file: string, key: string, value: any) {
70 | this.load();
71 | let data = this.dataForFile(file);
72 | data[key] = value;
73 | this.save();
74 | }
75 |
76 | private systemFile(): string {
77 | return `${this.path()}/serenade.json`;
78 | }
79 |
80 | private userFile(): string {
81 | return `${this.path()}/settings.json`;
82 | }
83 |
84 | getAnimations(): boolean {
85 | return this.get("user", "animations");
86 | }
87 |
88 | getAtom(): boolean {
89 | return this.get("system", "atom");
90 | }
91 |
92 | getCode(): boolean {
93 | return this.get("system", "code");
94 | }
95 |
96 | getInstalled(): boolean {
97 | return this.get("system", "installed");
98 | }
99 |
100 | path(): string {
101 | return `${os.homedir()}/.serenade`;
102 | }
103 |
104 | setPluginInstalled(plugin: string) {
105 | this.load();
106 | let data = this.dataForFile("system");
107 | if (!data.plugins) {
108 | data.plugins = [];
109 | }
110 |
111 | if (!data.plugins.includes(plugin)) {
112 | data.plugins.push(plugin);
113 | }
114 |
115 | this.save();
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/app.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import CommandHandler from "./command-handler";
3 | import IPC from "./ipc";
4 | import Settings from "./settings";
5 |
6 | export default class App {
7 | private commandHandler?: CommandHandler;
8 | private ipc?: IPC;
9 | private settings?: Settings;
10 | private initialized: boolean = false;
11 |
12 | private checkInstalled(): boolean {
13 | const installed = this.settings!.getInstalled();
14 | if (!installed) {
15 | this.showInstallMessage();
16 | return false;
17 | }
18 |
19 | return true;
20 | }
21 |
22 | private installHtml(): string {
23 | return `
24 |
25 |
26 |
27 |
28 |
29 |
30 | Serenade
31 |
64 |
65 |
66 | Welcome to Serenade!
67 | With Serenade, you can write code faster—by speaking in plain English, rather than typing. Use Serenade as your coding assistant, or abandon your keyboard entirely.
68 | To get started, download the Serenade app and run it alongside VS Code.
69 | Download
70 |
71 |
82 |
83 | `;
84 | }
85 |
86 | showInstallMessage() {
87 | const panel = vscode.window.createWebviewPanel(
88 | "serenade-install",
89 | "Serenade",
90 | vscode.ViewColumn.Two,
91 | {
92 | enableScripts: true,
93 | }
94 | );
95 |
96 | panel.webview.html = this.installHtml();
97 | panel.webview.onDidReceiveMessage((message: any) => {
98 | if (message.type == "download") {
99 | vscode.env.openExternal(vscode.Uri.parse("https://serenade.ai/download"));
100 | panel.dispose();
101 | }
102 | });
103 | }
104 |
105 | start() {
106 | if (this.initialized) {
107 | return;
108 | }
109 |
110 | this.initialized = true;
111 | this.settings = new Settings();
112 | this.commandHandler = new CommandHandler(this.settings);
113 | this.ipc = new IPC(this.commandHandler, "vscode");
114 |
115 | this.ipc.start();
116 | this.checkInstalled();
117 | this.commandHandler.pollActiveEditor();
118 | this.settings.setPluginInstalled("vscode");
119 |
120 | vscode.window.onDidChangeActiveTextEditor(() => {
121 | this.ipc!.sendActive();
122 | });
123 |
124 | vscode.window.onDidChangeWindowState((state) => {
125 | if (state.focused) {
126 | this.ipc!.sendActive();
127 | }
128 | });
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/diff.ts:
--------------------------------------------------------------------------------
1 | const DiffMatchPatch = require("./diff_match_patch");
2 |
3 | export enum DiffRangeType {
4 | Add,
5 | Delete,
6 | }
7 |
8 | export enum DiffHighlightType {
9 | Line,
10 | Word,
11 | }
12 |
13 | export class DiffPoint {
14 | row: number;
15 | column: number;
16 |
17 | constructor(row: number, column: number) {
18 | this.row = row;
19 | this.column = column;
20 | }
21 |
22 | static fromArray(range: number[]): DiffPoint {
23 | return new DiffPoint(range[0], range[1]);
24 | }
25 | }
26 |
27 | export class DiffRange {
28 | diffRangeType: DiffRangeType;
29 | diffHighlightType: DiffHighlightType;
30 | start: DiffPoint;
31 | stop: DiffPoint;
32 |
33 | constructor(
34 | diffRangeType: DiffRangeType,
35 | diffHighlightType: DiffHighlightType,
36 | start: DiffPoint,
37 | stop: DiffPoint
38 | ) {
39 | this.diffRangeType = diffRangeType;
40 | this.diffHighlightType = diffHighlightType;
41 | this.start = start;
42 | this.stop = stop;
43 | }
44 |
45 | static fromDiff(
46 | diffRangeType: DiffRangeType,
47 | source: string,
48 | index: number,
49 | length: number
50 | ): DiffRange {
51 | const start = cursorToRowAndColumn(source, index);
52 | const stop = cursorToRowAndColumn(source, index + length);
53 |
54 | // fix offsets for diffs occurring on the same line to be an exclusive range
55 | if (start[0] == stop[0]) {
56 | start[1]++;
57 | stop[1]++;
58 | }
59 |
60 | // if we're at a newline or the end, then start at the start of the next line
61 | let diffHighlightType = start[0] == stop[0] ? DiffHighlightType.Word : DiffHighlightType.Line;
62 | if (index + length == source.length - 1 || source[index] == "\n") {
63 | start[0]++;
64 | start[1] = 0;
65 | diffHighlightType = DiffHighlightType.Line;
66 |
67 | // make sure the entire line is consumed
68 | if (start[0] == stop[0] || index + length == source.length - 1) {
69 | stop[0]++;
70 | stop[1] = 0;
71 | }
72 | }
73 |
74 | return new DiffRange(
75 | diffRangeType,
76 | diffHighlightType,
77 | DiffPoint.fromArray(start),
78 | DiffPoint.fromArray(stop)
79 | );
80 | }
81 | }
82 |
83 | export function cursorToRowAndColumn(source: string, cursor: number): number[] {
84 | // iterate until the given substring index, incrementing rows and columns as we go
85 | let row = 0;
86 | let column = 0;
87 | for (let i = 0; i < cursor; i++) {
88 | column++;
89 | // Handle CRLF or LF
90 | if (source[i] == "\r" && i + 1 < source.length && source[i + 1] == "\n") {
91 | row += 1;
92 | column = 0;
93 | i++;
94 | } else if (source[i] == "\n") {
95 | row++;
96 | column = 0;
97 | }
98 | }
99 |
100 | return [row, column];
101 | }
102 |
103 | export function diff(before: string, after: string): DiffRange[] {
104 | const diffMatchPatch = new DiffMatchPatch();
105 | let diffs = diffMatchPatch.diff_main(before, after);
106 | diffMatchPatch.diff_cleanupSemantic(diffs);
107 |
108 | let result = [];
109 | let beforeIndex = -1;
110 | let afterIndex = -1;
111 |
112 | for (const entry of diffs) {
113 | if (entry[0] == 0) {
114 | beforeIndex += entry[1].length;
115 | afterIndex += entry[1].length;
116 | } else if (entry[0] == 1) {
117 | result.push(DiffRange.fromDiff(DiffRangeType.Add, after, afterIndex, entry[1].length));
118 | afterIndex += entry[1].length;
119 | } else if (entry[0] == -1) {
120 | result.push(DiffRange.fromDiff(DiffRangeType.Delete, before, beforeIndex, entry[1].length));
121 | beforeIndex += entry[1].length;
122 | }
123 | }
124 |
125 | return result;
126 | }
127 |
--------------------------------------------------------------------------------
/src/ipc.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from "uuid";
2 | import NodeWebSocket from "ws";
3 |
4 | export default class IPC {
5 | private app: string;
6 | private commandHandler: any;
7 | private connected: boolean = false;
8 | private id: string = "";
9 | private websocket?: WebSocket | NodeWebSocket;
10 | private url: string = "ws://localhost:17373/";
11 |
12 | constructor(commandHandler: any, app: string) {
13 | this.commandHandler = commandHandler;
14 | this.app = app;
15 | this.id = uuidv4();
16 | }
17 |
18 | isConnected() {
19 | return this.connected;
20 | }
21 |
22 | private onClose() {
23 | this.connected = false;
24 | }
25 |
26 | private async onMessage(message: any) {
27 | if (typeof message === "string") {
28 | let request;
29 | try {
30 | request = JSON.parse(message);
31 | } catch (e) {
32 | return;
33 | }
34 |
35 | if (request.message === "response") {
36 | const result = await this.handle(request.data.response);
37 | if (result) {
38 | this.send("callback", {
39 | callback: request.data.callback,
40 | data: result,
41 | });
42 | }
43 | }
44 | }
45 | }
46 |
47 | private onOpen() {
48 | this.connected = true;
49 | this.sendActive();
50 | }
51 |
52 | ensureConnection() {
53 | if (this.connected) {
54 | return;
55 | }
56 |
57 | try {
58 | if (typeof WebSocket !== "undefined" && WebSocket) {
59 | this.websocket = new WebSocket(this.url);
60 |
61 | this.websocket.addEventListener("open", () => {
62 | this.onOpen();
63 | });
64 |
65 | this.websocket.addEventListener("close", () => {
66 | this.onClose();
67 | });
68 |
69 | this.websocket.addEventListener("message", (event) => {
70 | this.onMessage(event.data);
71 | });
72 | } else {
73 | this.websocket = new NodeWebSocket(this.url);
74 |
75 | this.websocket.on("open", () => {
76 | this.onOpen();
77 | });
78 |
79 | this.websocket.on("close", () => {
80 | this.onClose();
81 | });
82 |
83 | this.websocket.on("message", (message) => {
84 | this.onMessage(message);
85 | });
86 | }
87 | } catch (e) {}
88 | }
89 |
90 | async handle(response: any): Promise {
91 | let result = null;
92 | if (response.execute) {
93 | for (const command of response.execute.commandsList) {
94 | if (command.type in (this.commandHandler as any)) {
95 | result = await (this.commandHandler as any)[command.type](command);
96 | }
97 | }
98 | }
99 |
100 | if (result) {
101 | return result;
102 | }
103 |
104 | return {
105 | message: "completed",
106 | data: {},
107 | };
108 | }
109 |
110 | sendActive() {
111 | let result = this.send("active", {
112 | app: this.app,
113 | id: this.id,
114 | });
115 | }
116 |
117 | send(message: string, data: any) {
118 | if (!this.connected || !this.websocket || this.websocket!.readyState != 1) {
119 | return false;
120 | }
121 |
122 | try {
123 | this.websocket!.send(JSON.stringify({ message, data }));
124 | return true;
125 | } catch (e) {
126 | this.connected = false;
127 | return false;
128 | }
129 | }
130 |
131 | start() {
132 | this.ensureConnection();
133 |
134 | setInterval(() => {
135 | this.ensureConnection();
136 | }, 1000);
137 |
138 | setInterval(() => {
139 | this.send("heartbeat", {
140 | app: this.app,
141 | id: this.id,
142 | });
143 | }, 60 * 1000);
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/command-handler.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from "vscode";
2 | import * as diff from "./diff";
3 | import Settings from "./settings";
4 | const ignoreParser: any = require("gitignore-globs");
5 |
6 | export default class CommandHandler {
7 | private activeEditor?: vscode.TextEditor;
8 | private errorColor: string = "255, 99, 71";
9 | private openFileList: any[] = [];
10 | private successColor: string = "43, 161, 67";
11 | private languageToExtension: { [key: string]: string[] } = {
12 | c: ["c", "h"],
13 | cpp: ["cpp", "cc", "cxx", "c++", "hpp", "hh", "hxx", "h++"],
14 | csharp: ["cs"],
15 | css: ["css", "scss"],
16 | dart: ["dart"],
17 | go: ["go"],
18 | html: ["html", "vue", "svelte"],
19 | java: ["java"],
20 | javascript: ["js", "jsx"],
21 | javascriptreact: ["jsx", "js"],
22 | jsx: ["jsx", "js"],
23 | kotlin: ["kt"],
24 | python: ["py"],
25 | ruby: ["rb"],
26 | rust: ["rs"],
27 | scss: ["scss"],
28 | shellscript: ["sh", "bash"],
29 | typescript: ["ts", "tsx"],
30 | typescriptreact: ["tsx", "ts"],
31 | vue: ["vue", "html"],
32 | };
33 |
34 | constructor(private settings: Settings) {}
35 |
36 | private async focus(): Promise {
37 | this.updateActiveEditor();
38 | if (!this.activeEditor) {
39 | return;
40 | }
41 |
42 | await vscode.window.showTextDocument(this.activeEditor!.document);
43 | await this.uiDelay();
44 | }
45 |
46 | private highlightRanges(ranges: diff.DiffRange[]): number {
47 | const duration = 300;
48 | const steps = [1, 2, 1];
49 | const step = duration / steps.length;
50 | const editor = vscode.window.activeTextEditor;
51 | if (!editor || ranges.length == 0) {
52 | return 0;
53 | }
54 |
55 | for (const range of ranges) {
56 | const decorations = steps.map((e) =>
57 | vscode.window.createTextEditorDecorationType({
58 | backgroundColor: `rgba(${
59 | range.diffRangeType == diff.DiffRangeType.Delete ? this.errorColor : this.successColor
60 | }, 0.${e})`,
61 | isWholeLine: range.diffHighlightType == diff.DiffHighlightType.Line,
62 | })
63 | );
64 |
65 | // atom and vs code use different types of ranges
66 | if (range.diffHighlightType == diff.DiffHighlightType.Line) {
67 | range.stop.row--;
68 | }
69 |
70 | for (let i = 0; i < steps.length; i++) {
71 | setTimeout(() => {
72 | this.activeEditor!.setDecorations(decorations[i], [
73 | new vscode.Range(
74 | range.start.row,
75 | range.start.column,
76 | range.stop.row,
77 | range.stop.column
78 | ),
79 | ]);
80 |
81 | setTimeout(() => {
82 | decorations[i].dispose();
83 | }, step);
84 | }, i * step);
85 | }
86 | }
87 |
88 | return 400;
89 | }
90 |
91 | private rowAndColumnToCursor(row: number, column: number, text: string) {
92 | // iterate through text, incrementing rows when newlines are found, and counting columns when row is right
93 | let cursor = 0;
94 | let currentRow = 0;
95 | let currentColumn = 0;
96 | for (let i = 0; i < text.length; i++) {
97 | if (currentRow === row) {
98 | if (currentColumn === column) {
99 | break;
100 | }
101 |
102 | currentColumn++;
103 | }
104 |
105 | if (text[i] === "\n") {
106 | currentRow++;
107 | }
108 |
109 | cursor++;
110 | }
111 |
112 | return cursor;
113 | }
114 |
115 | private async scrollToCursor(): Promise {
116 | if (!this.activeEditor) {
117 | return;
118 | }
119 |
120 | const cursor = this.activeEditor!.selection.start.line;
121 | if (this.activeEditor!.visibleRanges.length > 0) {
122 | const range = this.activeEditor!.visibleRanges[0];
123 | const buffer = 5;
124 | if (cursor < range.start.line + buffer || cursor > range.end.line - buffer) {
125 | await vscode.commands.executeCommand("revealLine", {
126 | lineNumber: cursor,
127 | at: "center",
128 | });
129 | }
130 | }
131 | }
132 |
133 | private async setSourceAndCursor(
134 | before: string,
135 | source: string,
136 | row: number,
137 | column: number
138 | ): Promise {
139 | if (!this.activeEditor) {
140 | return;
141 | }
142 |
143 | if (before != source) {
144 | await this.activeEditor.edit((edit) => {
145 | // Compute the shared prefix and postfix, so we can swap out the range between them and not disrupt
146 | // the rust analyzer as much.
147 | let startIndex = 0;
148 | while (
149 | startIndex < before.length &&
150 | startIndex < source.length &&
151 | before[startIndex] == source[startIndex]
152 | ) {
153 | startIndex++;
154 | }
155 | let stopOffset = 0;
156 | while (
157 | before.length - stopOffset - 1 >= startIndex &&
158 | source.length - stopOffset - 1 >= startIndex &&
159 | before[before.length - stopOffset - 1] == source[source.length - stopOffset - 1]
160 | ) {
161 | stopOffset++;
162 | }
163 | // don't split crlf when we strip them in serenade.
164 | if (before.length - stopOffset - 1 >= 0 &&
165 | before[before.length - stopOffset - 1] == "\r") {
166 | stopOffset--;
167 | }
168 | const [startLine, startCharacter] = diff.cursorToRowAndColumn(before, startIndex);
169 | const [stopLine, stopCharacter] = diff.cursorToRowAndColumn(
170 | before,
171 | before.length - stopOffset
172 | );
173 |
174 | const textRange = new vscode.Range(startLine, startCharacter, stopLine, stopCharacter);
175 | edit.replace(textRange, source.substring(startIndex, source.length - stopOffset));
176 | });
177 | }
178 |
179 | this.activeEditor.selections = [new vscode.Selection(row, column, row, column)];
180 | }
181 |
182 | private async uiDelay(timeout: number = 100): Promise {
183 | return new Promise((resolve) => {
184 | setTimeout(() => {
185 | resolve();
186 | }, timeout);
187 | });
188 | }
189 |
190 | private updateActiveEditor() {
191 | const editor = vscode.window.activeTextEditor;
192 | if (!editor) {
193 | return;
194 | }
195 |
196 | this.activeEditor = editor;
197 | }
198 |
199 | async COMMAND_TYPE_CLOSE_TAB(_data: any): Promise {
200 | await this.focus();
201 | await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
202 | await this.uiDelay();
203 | this.updateActiveEditor();
204 | }
205 |
206 | async COMMAND_TYPE_CLOSE_WINDOW(_data: any): Promise {
207 | await this.focus();
208 | await vscode.commands.executeCommand("workbench.action.closeActiveEditor");
209 | await this.uiDelay();
210 | this.updateActiveEditor();
211 | }
212 |
213 | async COMMAND_TYPE_COPY(data: any): Promise {
214 | if (data && data.text) {
215 | vscode.env.clipboard.writeText(data.text);
216 | }
217 |
218 | await this.uiDelay();
219 | }
220 |
221 | async COMMAND_TYPE_CREATE_TAB(_data: any): Promise {
222 | await this.focus();
223 | await vscode.commands.executeCommand("workbench.action.files.newUntitledFile");
224 | await this.uiDelay();
225 | this.updateActiveEditor();
226 | }
227 |
228 | async COMMAND_TYPE_DUPLICATE_TAB(_data: any): Promise {}
229 |
230 | async COMMAND_TYPE_GET_EDITOR_STATE(data: any): Promise {
231 | let result: any = {
232 | message: "editorState",
233 | data: {
234 | source: "",
235 | cursor: 0,
236 | selectionStart: 0,
237 | selectionEnd: 0,
238 | filename: "",
239 | },
240 | };
241 |
242 | if (!this.activeEditor) {
243 | return result;
244 | }
245 |
246 | let filename = this.activeEditor.document.fileName;
247 | const language = this.activeEditor.document.languageId;
248 | if (language && this.languageToExtension[language]) {
249 | if (!this.languageToExtension[language].some((e: string) => filename.endsWith(`.${e}`))) {
250 | filename = (filename || "file") + `.${this.languageToExtension[language][0]}`;
251 | }
252 | }
253 |
254 | result.data.filename = filename;
255 | if (data.limited) {
256 | return result;
257 | }
258 |
259 | // filter out the longest root that's a prefix of the filename
260 | const roots = vscode.workspace.workspaceFolders
261 | ? vscode.workspace.workspaceFolders.map((e: any) => e.uri.path)
262 | : [];
263 | let files = [];
264 | for (const file of this.openFileList.map((e: any) => e.path)) {
265 | let prefixLength = 0;
266 | for (const root of roots) {
267 | if (file.startsWith(root) && prefixLength < file.length) {
268 | prefixLength = root.length + 1;
269 | }
270 | }
271 |
272 | files.push(file.substring(prefixLength));
273 | }
274 | result.data.files = files;
275 |
276 | const source = this.activeEditor!.document.getText();
277 | const cursorPosition = this.activeEditor!.selection.active;
278 | const anchorPosition = this.activeEditor!.selection.anchor;
279 | const cursor = this.rowAndColumnToCursor(cursorPosition.line, cursorPosition.character, source);
280 | const anchor = this.rowAndColumnToCursor(anchorPosition.line, anchorPosition.character, source);
281 | if (cursor != anchor) {
282 | result.data.selectionStart = cursor > anchor ? anchor : cursor;
283 | result.data.selectionEnd = cursor < anchor ? anchor : cursor;
284 | }
285 |
286 | result.data.source = source;
287 | result.data.cursor = this.rowAndColumnToCursor(
288 | cursorPosition.line,
289 | cursorPosition.character,
290 | source
291 | );
292 | result.data.available = true;
293 | result.data.canGetState = true;
294 | result.data.canSetState = true;
295 | return result;
296 | }
297 |
298 | async COMMAND_TYPE_DEBUGGER_CONTINUE(_data: any): Promise {
299 | vscode.commands.executeCommand("workbench.action.debug.continue");
300 | }
301 |
302 | async COMMAND_TYPE_DEBUGGER_INLINE_BREAKPOINT(_data: any): Promise {
303 | await vscode.commands.executeCommand("editor.debug.action.toggleInlineBreakpoint");
304 | }
305 |
306 | async COMMAND_TYPE_DEBUGGER_PAUSE(_data: any): Promise {
307 | vscode.commands.executeCommand("workbench.action.debug.pause");
308 | }
309 |
310 | async COMMAND_TYPE_DEBUGGER_SHOW_HOVER(_data: any): Promise {
311 | await this.focus();
312 | vscode.commands.executeCommand("editor.debug.action.showDebugHover");
313 | }
314 |
315 | async COMMAND_TYPE_DEBUGGER_START(_data: any): Promise {
316 | vscode.commands.executeCommand("workbench.action.debug.start");
317 | }
318 |
319 | async COMMAND_TYPE_DEBUGGER_STEP_INTO(_data: any): Promise {
320 | vscode.commands.executeCommand("workbench.action.debug.stepInto");
321 | }
322 |
323 | async COMMAND_TYPE_DEBUGGER_STEP_OUT(_data: any): Promise {
324 | vscode.commands.executeCommand("workbench.action.debug.stepOut");
325 | }
326 |
327 | async COMMAND_TYPE_DEBUGGER_STEP_OVER(_data: any): Promise {
328 | vscode.commands.executeCommand("workbench.action.debug.stepOver");
329 | }
330 |
331 | async COMMAND_TYPE_DEBUGGER_STOP(_data: any): Promise {
332 | vscode.commands.executeCommand("workbench.action.debug.stop");
333 | }
334 |
335 | async COMMAND_TYPE_DEBUGGER_TOGGLE_BREAKPOINT(_data: any): Promise {
336 | vscode.commands.executeCommand("editor.debug.action.toggleBreakpoint");
337 | }
338 |
339 | async COMMAND_TYPE_DIFF(data: any): Promise {
340 | await this.focus();
341 | if (!this.activeEditor) {
342 | return;
343 | }
344 |
345 | const before = this.activeEditor.document.getText() || "";
346 | let [row, column] = diff.cursorToRowAndColumn(data.source, data.cursor);
347 | if (!this.settings.getAnimations()) {
348 | await this.setSourceAndCursor(before, data.source, row, column);
349 | await this.scrollToCursor();
350 | return;
351 | }
352 |
353 | let ranges = diff.diff(before, data.source);
354 | if (ranges.length == 0) {
355 | ranges = [
356 | new diff.DiffRange(
357 | diff.DiffRangeType.Add,
358 | diff.DiffHighlightType.Line,
359 | new diff.DiffPoint(row, 0),
360 | new diff.DiffPoint(row + 1, 0)
361 | ),
362 | ];
363 | }
364 |
365 | const addRanges = ranges.filter(
366 | (e: diff.DiffRange) => e.diffRangeType == diff.DiffRangeType.Add
367 | );
368 |
369 | const deleteRanges = ranges.filter(
370 | (e: diff.DiffRange) => e.diffRangeType == diff.DiffRangeType.Delete
371 | );
372 |
373 | const timeout = this.highlightRanges(deleteRanges);
374 | return new Promise((resolve) => {
375 | setTimeout(
376 | async () => {
377 | await this.setSourceAndCursor(before, data.source, row, column);
378 | this.highlightRanges(addRanges);
379 | await this.scrollToCursor();
380 | resolve(null);
381 | },
382 | deleteRanges.length > 0 ? timeout : 1
383 | );
384 | });
385 | }
386 |
387 | async COMMAND_TYPE_EVALUATE_IN_PLUGIN(data: any): Promise {
388 | vscode.commands.executeCommand(data.text);
389 | }
390 |
391 | async COMMAND_TYPE_GO_TO_DEFINITION(_data: any): Promise {
392 | await this.focus();
393 | await vscode.commands.executeCommand("editor.action.revealDefinition");
394 | }
395 |
396 | async COMMAND_TYPE_NEXT_TAB(_data: any): Promise {
397 | await this.focus();
398 | await vscode.commands.executeCommand("workbench.action.nextEditor");
399 | await this.uiDelay();
400 | this.updateActiveEditor();
401 | }
402 |
403 | async COMMAND_TYPE_OPEN_FILE(data: any): Promise {
404 | await vscode.window.showTextDocument(this.openFileList[data.index || 0]);
405 | }
406 |
407 | async COMMAND_TYPE_OPEN_FILE_LIST(data: any): Promise {
408 | await this.focus();
409 |
410 | const path = data.path
411 | .replace(/\//, "*/*")
412 | .split("")
413 | .map((e: string) => {
414 | if (e == " ") {
415 | return "*";
416 | } else if (e.match(/[a-z]/)) {
417 | return `{${e.toUpperCase()},${e.toLowerCase()}}`;
418 | }
419 |
420 | return e;
421 | })
422 | .join("");
423 |
424 | let exclude: string[] = [
425 | "**/.git",
426 | "**/.hg",
427 | "**/node_modules",
428 | "**/npm_packages",
429 | "**/npm",
430 | ...Object.keys(
431 | (await vscode.workspace.getConfiguration("search", null).get("exclude")) || {}
432 | ),
433 | ...Object.keys((await vscode.workspace.getConfiguration("files", null).get("exclude")) || {}),
434 | ];
435 |
436 | const ignorePath = await vscode.workspace.findFiles(".gitignore");
437 | if (ignorePath.length > 0) {
438 | exclude = exclude.concat(
439 | ignoreParser._map(
440 | ignoreParser._prepare(
441 | Buffer.from(await vscode.workspace.fs.readFile(ignorePath[0]))
442 | .toString("utf-8")
443 | .split("\n")
444 | )
445 | )
446 | );
447 | }
448 |
449 | this.openFileList = await vscode.workspace.findFiles(
450 | `**/*${path}*`,
451 | `{${exclude.map((e: string) => e.replace(/\\/g, "/")).join(",")}}`,
452 | 10
453 | );
454 |
455 | return { message: "sendText", data: { text: "callback open" } };
456 | }
457 |
458 | async COMMAND_TYPE_PREVIOUS_TAB(_data: any): Promise {
459 | await this.focus();
460 | await vscode.commands.executeCommand("workbench.action.previousEditor");
461 | await this.uiDelay();
462 | this.updateActiveEditor();
463 | }
464 |
465 | async COMMAND_TYPE_REDO(_data: any): Promise {
466 | await this.focus();
467 | await vscode.commands.executeCommand("redo");
468 | await this.scrollToCursor();
469 | }
470 |
471 | async COMMAND_TYPE_SAVE(_data: any): Promise {
472 | await this.focus();
473 | await vscode.commands.executeCommand("workbench.action.files.save");
474 | }
475 |
476 | async COMMAND_TYPE_SELECT(data: any): Promise {
477 | if (!this.activeEditor) {
478 | return;
479 | }
480 |
481 | const [startRow, startColumn] = diff.cursorToRowAndColumn(data.source, data.cursor);
482 | const [endRow, endColumn] = diff.cursorToRowAndColumn(data.source, data.cursorEnd);
483 | this.activeEditor!.selections = [
484 | new vscode.Selection(startRow, startColumn, endRow, endColumn),
485 | ];
486 | }
487 |
488 | async COMMAND_TYPE_SPLIT(data: any): Promise {
489 | await this.focus();
490 | const direction = data.direction.toLowerCase();
491 | const split = direction.charAt(0).toUpperCase() + direction.slice(1);
492 | await vscode.commands.executeCommand(`workbench.action.splitEditor${split}`);
493 | await this.uiDelay();
494 | this.updateActiveEditor();
495 | }
496 |
497 | async COMMAND_TYPE_STYLE(_data: any): Promise {
498 | await this.focus();
499 | await vscode.commands.executeCommand("editor.action.formatDocument");
500 | await this.uiDelay();
501 | }
502 |
503 | async COMMAND_TYPE_SWITCH_TAB(data: any): Promise {
504 | await this.focus();
505 | if (data.index <= 0) {
506 | await vscode.commands.executeCommand("workbench.action.lastEditorInGroup");
507 | } else {
508 | await vscode.commands.executeCommand(`workbench.action.openEditorAtIndex${data.index}`);
509 | }
510 |
511 | await this.uiDelay();
512 | this.updateActiveEditor();
513 | }
514 |
515 | async COMMAND_TYPE_UNDO(_data: any): Promise {
516 | await this.focus();
517 | await vscode.commands.executeCommand("undo");
518 | await this.scrollToCursor();
519 | }
520 |
521 | async COMMAND_TYPE_WINDOW(data: any): Promise {
522 | await this.focus();
523 | const direction = data.direction.toLowerCase();
524 | const split = direction.charAt(0).toUpperCase() + direction.slice(1);
525 | await vscode.commands.executeCommand(`workspace.action.focus${split}Group`);
526 | await this.uiDelay();
527 | this.updateActiveEditor();
528 | }
529 |
530 | pollActiveEditor() {
531 | setInterval(() => {
532 | this.updateActiveEditor();
533 | }, 1000);
534 | }
535 | }
536 |
--------------------------------------------------------------------------------
/src/diff_match_patch.js:
--------------------------------------------------------------------------------
1 | var diff_match_patch=function(){this.Diff_Timeout=1;this.Diff_EditCost=4;this.Match_Threshold=.5;this.Match_Distance=1E3;this.Patch_DeleteThreshold=.5;this.Patch_Margin=4;this.Match_MaxBits=32},DIFF_DELETE=-1,DIFF_INSERT=1,DIFF_EQUAL=0;diff_match_patch.Diff=function(a,b){this[0]=a;this[1]=b};diff_match_patch.Diff.prototype.length=2;diff_match_patch.Diff.prototype.toString=function(){return this[0]+","+this[1]};
2 | diff_match_patch.prototype.diff_main=function(a,b,c,d){"undefined"==typeof d&&(d=0>=this.Diff_Timeout?Number.MAX_VALUE:(new Date).getTime()+1E3*this.Diff_Timeout);if(null==a||null==b)throw Error("Null input. (diff_main)");if(a==b)return a?[new diff_match_patch.Diff(DIFF_EQUAL,a)]:[];"undefined"==typeof c&&(c=!0);var e=c,f=this.diff_commonPrefix(a,b);c=a.substring(0,f);a=a.substring(f);b=b.substring(f);f=this.diff_commonSuffix(a,b);var g=a.substring(a.length-f);a=a.substring(0,a.length-f);b=b.substring(0,
3 | b.length-f);a=this.diff_compute_(a,b,e,d);c&&a.unshift(new diff_match_patch.Diff(DIFF_EQUAL,c));g&&a.push(new diff_match_patch.Diff(DIFF_EQUAL,g));this.diff_cleanupMerge(a);return a};
4 | diff_match_patch.prototype.diff_compute_=function(a,b,c,d){if(!a)return[new diff_match_patch.Diff(DIFF_INSERT,b)];if(!b)return[new diff_match_patch.Diff(DIFF_DELETE,a)];var e=a.length>b.length?a:b,f=a.length>b.length?b:a,g=e.indexOf(f);return-1!=g?(c=[new diff_match_patch.Diff(DIFF_INSERT,e.substring(0,g)),new diff_match_patch.Diff(DIFF_EQUAL,f),new diff_match_patch.Diff(DIFF_INSERT,e.substring(g+f.length))],a.length>b.length&&(c[0][0]=c[2][0]=DIFF_DELETE),c):1==f.length?[new diff_match_patch.Diff(DIFF_DELETE,
5 | a),new diff_match_patch.Diff(DIFF_INSERT,b)]:(e=this.diff_halfMatch_(a,b))?(b=e[1],f=e[3],a=e[4],e=this.diff_main(e[0],e[2],c,d),c=this.diff_main(b,f,c,d),e.concat([new diff_match_patch.Diff(DIFF_EQUAL,a)],c)):c&&100c);t++){for(var v=-t+p;v<=t-x;v+=2){var n=f+v;var r=v==-t||v!=t&&h[n-1]d)x+=2;else if(y>e)p+=2;else if(m&&(n=f+k-v,0<=n&&n=
9 | u)return this.diff_bisectSplit_(a,b,r,y,c)}}for(v=-t+w;v<=t-q;v+=2){n=f+v;u=v==-t||v!=t&&l[n-1]d)q+=2;else if(r>e)w+=2;else if(!m&&(n=f+k-v,0<=n&&n=u)))return this.diff_bisectSplit_(a,b,r,y,c)}}return[new diff_match_patch.Diff(DIFF_DELETE,a),new diff_match_patch.Diff(DIFF_INSERT,b)]};
10 | diff_match_patch.prototype.diff_bisectSplit_=function(a,b,c,d,e){var f=a.substring(0,c),g=b.substring(0,d);a=a.substring(c);b=b.substring(d);f=this.diff_main(f,g,!1,e);e=this.diff_main(a,b,!1,e);return f.concat(e)};
11 | diff_match_patch.prototype.diff_linesToChars_=function(a,b){function c(a){for(var b="",c=0,g=-1,h=d.length;gd?a=a.substring(c-d):c=a.length?[h,k,l,m,g]:null}if(0>=this.Diff_Timeout)return null;
16 | var d=a.length>b.length?a:b,e=a.length>b.length?b:a;if(4>d.length||2*e.lengthd[4].length?g:d:d:g;else return null;if(a.length>b.length){d=g[0];e=g[1];var h=g[2];var l=g[3]}else h=g[0],l=g[1],d=g[2],e=g[3];return[d,e,h,l,g[4]]};
17 | diff_match_patch.prototype.diff_cleanupSemantic=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=0,h=0,l=0,k=0;f=e){if(d>=b.length/2||d>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,c.substring(0,d))),a[f-1][1]=b.substring(0,b.length-d),a[f+1][1]=c.substring(d),f++}else if(e>=b.length/2||e>=c.length/2)a.splice(f,0,new diff_match_patch.Diff(DIFF_EQUAL,b.substring(0,e))),a[f-1][0]=DIFF_INSERT,a[f-1][1]=c.substring(0,c.length-e),a[f+1][0]=DIFF_DELETE,
19 | a[f+1][1]=b.substring(e),f++;f++}f++}};
20 | diff_match_patch.prototype.diff_cleanupSemanticLossless=function(a){function b(a,b){if(!a||!b)return 6;var c=a.charAt(a.length-1),d=b.charAt(0),e=c.match(diff_match_patch.nonAlphaNumericRegex_),f=d.match(diff_match_patch.nonAlphaNumericRegex_),g=e&&c.match(diff_match_patch.whitespaceRegex_),h=f&&d.match(diff_match_patch.whitespaceRegex_);c=g&&c.match(diff_match_patch.linebreakRegex_);d=h&&d.match(diff_match_patch.linebreakRegex_);var k=c&&a.match(diff_match_patch.blanklineEndRegex_),l=d&&b.match(diff_match_patch.blanklineStartRegex_);
21 | return k||l?5:c||d?4:e&&!g&&h?3:g||h?2:e||f?1:0}for(var c=1;c=k&&(k=m,g=d,h=e,l=f)}a[c-1][1]!=g&&(g?a[c-1][1]=g:(a.splice(c-
22 | 1,1),c--),a[c][1]=h,l?a[c+1][1]=l:(a.splice(c+1,1),c--))}c++}};diff_match_patch.nonAlphaNumericRegex_=/[^a-zA-Z0-9]/;diff_match_patch.whitespaceRegex_=/\s/;diff_match_patch.linebreakRegex_=/[\r\n]/;diff_match_patch.blanklineEndRegex_=/\n\r?\n$/;diff_match_patch.blanklineStartRegex_=/^\r?\n\r?\n/;
23 | diff_match_patch.prototype.diff_cleanupEfficiency=function(a){for(var b=!1,c=[],d=0,e=null,f=0,g=!1,h=!1,l=!1,k=!1;fb)break;e=c;f=d}return a.length!=g&&a[g][0]===DIFF_DELETE?f:f+(b-e)};
28 | diff_match_patch.prototype.diff_prettyHtml=function(a){for(var b=[],c=/&/g,d=//g,f=/\n/g,g=0;g");switch(h){case DIFF_INSERT:b[g]=''+l+"";break;case DIFF_DELETE:b[g]=''+l+"";break;case DIFF_EQUAL:b[g]=""+l+""}}return b.join("")};
29 | diff_match_patch.prototype.diff_text1=function(a){for(var b=[],c=0;cl)throw Error("Invalid number in diff_fromDelta: "+h);h=a.substring(e,e+=l);"="==f[g].charAt(0)?c[d++]=new diff_match_patch.Diff(DIFF_EQUAL,h):c[d++]=
33 | new diff_match_patch.Diff(DIFF_DELETE,h);break;default:if(f[g])throw Error("Invalid diff operation in diff_fromDelta: "+f[g]);}}if(e!=a.length)throw Error("Delta length ("+e+") does not equal source text length ("+a.length+").");return c};diff_match_patch.prototype.match_main=function(a,b,c){if(null==a||null==b||null==c)throw Error("Null input. (match_main)");c=Math.max(0,Math.min(c,a.length));return a==b?0:a.length?a.substring(c,c+b.length)==b?c:this.match_bitap_(a,b,c):-1};
34 | diff_match_patch.prototype.match_bitap_=function(a,b,c){function d(a,d){var e=a/b.length,g=Math.abs(c-d);return f.Match_Distance?e+g/f.Match_Distance:g?1:e}if(b.length>this.Match_MaxBits)throw Error("Pattern too long for this browser.");var e=this.match_alphabet_(b),f=this,g=this.Match_Threshold,h=a.indexOf(b,c);-1!=h&&(g=Math.min(d(0,h),g),h=a.lastIndexOf(b,c+b.length),-1!=h&&(g=Math.min(d(0,h),g)));var l=1<=k;q--){var t=e[a.charAt(q-1)];m[q]=0===w?(m[q+1]<<1|1)&t:(m[q+1]<<1|1)&t|(x[q+1]|x[q])<<1|1|x[q+1];if(m[q]&l&&(t=d(w,q-1),t<=g))if(g=t,h=q-1,h>c)k=Math.max(1,2*c-h);else break}if(d(w+1,c)>g)break;x=m}return h};
36 | diff_match_patch.prototype.match_alphabet_=function(a){for(var b={},c=0;c=2*this.Patch_Margin&&e&&(this.patch_addContext_(a,h),c.push(a),a=new diff_match_patch.patch_obj,e=0,h=d,f=g)}k!==DIFF_INSERT&&(f+=m.length);k!==DIFF_DELETE&&(g+=m.length)}e&&(this.patch_addContext_(a,h),c.push(a));return c};
42 | diff_match_patch.prototype.patch_deepCopy=function(a){for(var b=[],c=0;cthis.Match_MaxBits){var k=this.match_main(b,h.substring(0,this.Match_MaxBits),g);-1!=k&&(l=this.match_main(b,h.substring(h.length-this.Match_MaxBits),g+h.length-this.Match_MaxBits),-1==l||k>=l)&&(k=-1)}else k=this.match_main(b,h,
44 | g);if(-1==k)e[f]=!1,d-=a[f].length2-a[f].length1;else if(e[f]=!0,d=k-g,g=-1==l?b.substring(k,k+h.length):b.substring(k,l+this.Match_MaxBits),h==g)b=b.substring(0,k)+this.diff_text2(a[f].diffs)+b.substring(k+h.length);else if(g=this.diff_main(h,g,!1),h.length>this.Match_MaxBits&&this.diff_levenshtein(g)/h.length>this.Patch_DeleteThreshold)e[f]=!1;else{this.diff_cleanupSemanticLossless(g);h=0;var m;for(l=0;le[0][1].length){var f=b-e[0][1].length;e[0][1]=c.substring(e[0][1].length)+e[0][1];d.start1-=f;d.start2-=f;d.length1+=f;d.length2+=f}d=a[a.length-1];e=d.diffs;
47 | 0==e.length||e[e.length-1][0]!=DIFF_EQUAL?(e.push(new diff_match_patch.Diff(DIFF_EQUAL,c)),d.length1+=b,d.length2+=b):b>e[e.length-1][1].length&&(f=b-e[e.length-1][1].length,e[e.length-1][1]+=c.substring(0,f),d.length1+=f,d.length2+=f);return c};
48 | diff_match_patch.prototype.patch_splitMax=function(a){for(var b=this.Match_MaxBits,c=0;c2*b?(h.length1+=k.length,e+=k.length,l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),d.diffs.shift()):(k=k.substring(0,b-h.length1-this.Patch_Margin),h.length1+=k.length,e+=k.length,g===DIFF_EQUAL?(h.length2+=k.length,f+=k.length):l=!1,h.diffs.push(new diff_match_patch.Diff(g,k)),k==d.diffs[0][1]?d.diffs.shift():d.diffs[0][1]=d.diffs[0][1].substring(k.length))}g=this.diff_text2(h.diffs);
50 | g=g.substring(g.length-this.Patch_Margin);k=this.diff_text1(d.diffs).substring(0,this.Patch_Margin);""!==k&&(h.length1+=k.length,h.length2+=k.length,0!==h.diffs.length&&h.diffs[h.diffs.length-1][0]===DIFF_EQUAL?h.diffs[h.diffs.length-1][1]+=k:h.diffs.push(new diff_match_patch.Diff(DIFF_EQUAL,k)));l||a.splice(++c,0,h)}}};diff_match_patch.prototype.patch_toText=function(a){for(var b=[],c=0;c