$engine JScript
$uname VimComplete
$dname Автодополнение в стиле Vim
$addin global
$addin stdlib
stdlib.require('TextWindow.js', SelfScript);
/*===========================================================================
Скрипт: VimComplete.js
Версия: 1.4
Автор: Александр Кунташов
Описание:
Атодополнение слов в стиле редактора Vim
===========================================================================*/
/*
Макрос macrosСледующееСлово()
Подбирает часть слова слева от курсора и пытается дополнить его,
ища вперед по тексту слова, с такой же левой частью. Подставляет
первое подходящее. Следующий вызов макроса подставит следующее за
первым найденным словом и так далее по кругу (дойдя до последней строки
модуля поиск продолжится с первой строки).
Макрос macrosПредыдущееСлово()
Тоже самое, только поиск слов осуществляется в обратном направлении.
В классическом Vim используются следующие хоткеи:
Ctrl + N для дополнения с поиском вперед (следующее слово, Next word)
Ctrl + P для дополнения с поиском назад (предыдущее слово, Previous word)
*/
////////////////////////////////////////////////////////////////////////////////////////
//// Макросы
////
function macrosСледующееСлово() // Ctrl + N
{
return completeWord();
}
function macrosПредыдущееСлово() // Ctrl + P
{
//debugger;
return completeWord(true);
}
////////////////////////////////////////////////////////////////////////////////////////
//// Реализация функционала скрипта.
////
var CurDoc = null;
var VimComplete = new VimAutoCompletionTool();
function completeWord(lookBackward)
{
CurDoc = GetTextWindow();
if (!CurDoc)
return false;
VimComplete.completeWord(lookBackward);
CurDoc = null;
return true;
}
/* Возвращает часть слова слева от текущего положения курсора */
function getLeftWord(doc)
{
var cl, word = '';
var pos = doc.GetCaretPos();
cl = doc.GetLine(pos.beginRow);
/* ВАЖНО! Помним про индексацию: а именно, что позиция курсора индексируется с 1.
Символы же в JavaScript-строке - с 0. Координата предыдущего символа от курсора в терминах
позиции каретки = (beginCol - 1). Поскольку индексация в строке с 0, то индекс символа
в строке будет вычисляться как ((beginCol - 1) - 1) = (beginCol - 2). */
for (var i = pos.beginCol - 2;
(i >= 0) && cl.charAt(i).match(/[\wА-Яа-я]/i);
word = cl.charAt(i--) + word)
;
return word;
}
// Примитивный класс для выделения слов в строке, их фильтрации и последовательному перебору.
function Line(str)
{
var s = str;
var words = null;
this.reset = function ()
{
words = s.split(/[^\wА-я]+/);
if (!words.length || words.length == 1 && !words[0])
words = new Array;
}
this.assert = function (ix)
{
return ((typeof(words) == "object") && (ix >= 0) && (ix < words.length));
}
this.word = function (ix)
{
if (this.assert(ix)) return words[ix];
}
this.count = function ()
{
return words.length;
}
this.words = function ()
{
return words;
}
/* возвращает объект-итератор с единственным методом next()
c помощью которого осуществляется перебор строк (до тех пор,
пока не вернет значение undefined, означающее конец списка) */
this.iterator = function (r)
{
var collection = this;
return {
collection : collection,
iterator : r ? collection.count() : (-1),
next : function(reverse)
{
return this.collection.word( this.iterator += (reverse?(-1):1) );
}
}
}
/* фильтрует элементы, оставляя только те, значения которых
матчат шаблон pattern */
this.filter = function (pattern, unique)
{
var used = {};
if (this.assert(0)) {
var nw = new Array();
for (var i=0; i<this.count(); i++) {
if (this.word(i).match(pattern)) {
if (unique) {
if (!used[this.word(i)]) {
used[this.word(i)] = true;
nw[nw.length] = this.word(i);
}
}
else {
nw[nw.length] = this.word(i);
}
}
}
words = nw;
return true;
}
return false;
}
this.reset(); // инициализация
}
function VimAutoCompletionTool()
{
var srcDocPath; // путь до исходного документа (используется для идентификации документов)
var srcLine; // исходная строка документа
var srcCol; // первая позиция в строке перед исходным словом
var srcWord; // исходное слово (которое пытаемся дополнить)
var lastWord; // последнее использованное в подстановке слово
var curLineIx; // индекс текущей строки (из которой берутся соответствия)
var words; // список слов-соответствий текущей строки
var backwardSearch; // обратный поиск (по умолчанию поиск прямой, "вперед")
var pattern; // шаблон (регулярное выражение), описывающий соответствие исходному слову
var counter; // счетчик соответствий
var total; // общее число соответствий
/* выполняет (ре)инициализацию объекта, если это необходимо */
this.setup = function (lookBackward)
{
var word = getLeftWord(CurDoc);
if (this.isNewLoop(CurDoc, word)) { // реинициализация
srcDocPath = CurDoc.GetHwnd();
with (CurDoc.GetCaretPos())
{
srcLine = beginRow;
srcCol = (beginCol - 1) - word.length;
}
srcWord = word;
lastWord = word; // чтобы корректно сделать первую подстановку
curLineIx = srcLine;
pattern = new RegExp("^" + word, "i");
// начинаем искать соответствия начиная с исходной строки
words = this.parseLine(lookBackward ? this.leftPart() : this.rightPart());
// счетчики
counter = 0;
total = null;
}
backwardSearch = lookBackward;
}
/* условие необходимости произвести переинициализацию переменных членов объекта VimAutoCompletionTool */
this.isNewLoop = function (doc, word)
{
var pos = doc.GetCaretPos();
return !(words && (srcDocPath == doc.GetHwnd())
&& (srcLine == pos.beginRow) && (lastWord == word)
&& (srcCol == (pos.beginCol - 1 - word.length)));
}
/* проверяет, не выходит ли индекс строки за допустимые границы */
this.assert = function (lIx)
{
return (CurDoc && (1 <= lIx) && (lIx <= CurDoc.LinesCount()));
}
/* берет следующее соответствие и подставляет его на место исходного слова */
this.completeWord = function (lookBackward)
{
this.setup(lookBackward);
while (true) {
var word = words.next(lookBackward);
if (word) {
this.complete(word);
return;
}
words = this.nextLine();
if (!words.collection.count())
return;
}
}
/* строит и возвращает список соответсвующих слов для следующей по порядку строки */
this.nextLine = function ()
{
curLineIx += (backwardSearch ? -1 : 1);
if (backwardSearch) {
if (curLineIx < 1) {
curLineIx = CurDoc.LinesCount();
}
}
else {
if (curLineIx > CurDoc.LinesCount()) {
curLineIx = 1;
}
}
return this.parseLine(this.curLine());
}
/* "разбирает" переданную в качестве параметра строку на слова и фильтрует их
в соотвествии с шаблоном, который описывает подходящие соответствия для исходного слова */
this.parseLine = function (srcLine)
{
var w = new Line(srcLine);
w.filter(pattern, true);
return w.iterator(backwardSearch);
}
/* выполняет подстановку очередного соответствия вместо исходного слова */
this.complete = function (word)
{
CurDoc.ReplaceLine(srcLine, this.leftPart() + word + this.rightPart());
CurDoc.SetCaretPos(srcLine, srcCol + word.length + 1); // Снова помним про индексы!
lastWord = word;
counter += backwardSearch ? -1 : 1;
if ((curLineIx == srcLine)&&(lastWord == srcWord)) {
if ((!total)&&counter) {
total = Math.abs(counter) - 1;
}
counter = 0;
}
}
/* возвращает текущую строку */
this.curLine = function ()
{
var str = "";
if (this.assert(curLineIx)) {
/* поскольку исходная строка у нас постоянно меняется,
ее "собираем" отдельно, возвращая на место исходное слово */
if (curLineIx == srcLine) {
str = this.leftPart() + srcWord + this.rightPart();
}
else {
str = CurDoc.GetLine(curLineIx);
}
}
return str;
}
/* левая половина строки, содержащей исходное слово; само исходное слово не включается */
this.leftPart = function ()
{
return CurDoc.Range(srcLine, 1, srcLine, srcCol).GetText();
}
/* правая половина строки, содержащей исходное слово; само исходное слово не включается */
this.rightPart = function ()
{
return CurDoc.Range(srcLine, srcCol + lastWord.length + 1, srcLine, CurDoc.GetLine(srcLine).length + 1).GetText();
}
}