select-part-of-word
Version:
An extension that allows you to move selection by a word in camel-case string.
284 lines (244 loc) • 8.22 kB
JavaScript
const common = require( './common' ),
vscode = require( 'vscode' ),
CHAR_TYPE = common.CHAR_TYPE;
function reverseString( input ) {
return [ ...input ].reverse().join( '' );
}
module.exports = {
/**
* Moves the selection to the right side. It will always collapse the range.
*
* Reference point is the __active__ boundary of the selection.
*
* @param {TextEditor} textEditor
*/
moveRight( textEditor ) {
this._moveCommon( textEditor, true, false );
},
/**
* Moves the selection to the left side. It will always collapse the range.
*
* Reference point is the __active__ boundary of the selection.
*
* @param {TextEditor} textEditor
*/
moveLeft( textEditor ) {
this._moveCommon( textEditor, false, false );
},
/**
* Expands (or shrinks depending where range's active position is) the selection to the right side of text.
*
* @param {TextEditor} textEditor
*/
selectRight( textEditor ) {
this._moveCommon( textEditor, true, true );
},
/**
* Expands (or shrinks depending where range's active position is) the selection to the left side of text.
*
* @param {TextEditor} textEditor
*/
selectLeft( textEditor ) {
this._moveCommon( textEditor, false, true );
},
/**
* Deletes part of the word before the caret.
*
* @param {TextEditor} textEditor
* @param {TextEditorEdit} edits
*/
backspace( textEditor, edits ) {
this._moveCommon( textEditor, false, true );
// Now remove each anchor.
for ( let sel of textEditor.selections ) {
if ( !sel.isEmpty ) {
edits.delete( sel );
}
}
},
/**
* Deletes part of the word ahead from the caret.
*
* @param {TextEditor} textEditor
* @param {TextEditorEdit} edits
*/
delete( textEditor, edits ) {
this._moveCommon( textEditor, true, true );
// Now remove each anchor.
for ( let sel of textEditor.selections ) {
if ( !sel.isEmpty ) {
edits.delete( sel );
}
}
},
/**
* Since all `move*` and `seslect*` methods had a common loop, it has been extracted into this common method.
*
* @private
* @param {TextEditor} textEditor
* @param {Boolean} right Whether to move right or left.
* @param {Boolean} [preserveAnchor=false] Whether to preserve existing selection anchor points. If not every selection
* will become collapsed at position returned by `_movePositionRight` or `_movePositionRight`.
*/
_moveCommon( textEditor, right, preserveAnchor ) {
textEditor = this._getEditor( textEditor );
let moveMethod = right ? this._movePositionRight : this._movePositionLeft;
// Interestingly enough we need to override textEditor.selecitons. Calling push or anything
// won't change the selection (#19).
textEditor.selections = textEditor.selections.map( sel => {
let newPos = moveMethod.call( this, textEditor.document, sel.active );
// Update the selection if needed.
return newPos ? new vscode.Selection( preserveAnchor ? sel.anchor : newPos, newPos ) : sel;
}, this );
},
_movePositionRight( doc, position ) {
return this._movePosition( doc, position, true );
},
_movePositionLeft( doc, position ) {
return this._movePosition( doc, position, false );
},
/**
* A generic method for {@link _movePositionRight} and {@link _movePositionLeft}.
*
* @param {TextDocument} doc
* @param {Position} position
* @param {Boolean} [right]
*/
_movePosition( doc, position, right ) {
let linesGenerator = this._getAheadLines( doc, position, Boolean( right ) ),
endPos = null,
curLine,
textAhead;
for ( [ curLine, textAhead ] of linesGenerator ) {
if ( curLine === position.line ) {
// First line has some special handling.
endPos = this._movePositionFirstLine( doc, position, right, textAhead );
} else {
let match = textAhead.search( /[^\s]/ );
if ( match !== -1 ) {
endPos = right ? match : textAhead.length - match;
}
}
if ( endPos !== null ) {
break;
}
}
// Final fallback - after all the iteration if nothing can be matched, move sel to doc end / beginning.
if ( endPos === null ) {
if ( right ) {
return new vscode.Position( doc.lineCount - 1, doc.lineAt( doc.lineCount - 1 ).text.length );
} else {
return new vscode.Position( 0, 0 );
}
}
return new vscode.Position( curLine, endPos );
},
_movePositionFirstLine( doc, position, right, textAhead ) {
let endPos = null,
curCharType = this._getCharType( textAhead[ 0 ] || '' ),
farAheadCharType = this._getCharType( textAhead[ 1 ] || '' ),
match = textAhead.search( common.regExpExcludeMapping[ curCharType ] ),
// Empty textAhead means that the caret is at boundary position.
isBoundaryPosiiton = textAhead === '',
// Some matchings will require adjustment.
matchAdjustment = 0;
if ( right && farAheadCharType !== curCharType &&
curCharType !== common.CHAR_TYPE.WHITESPACE && farAheadCharType !== common.CHAR_TYPE.WHITESPACE ) {
// Catches a case like: foo^BarBaz.
match = textAhead.substr( 1 ).search( common.regExpExcludeMapping[ farAheadCharType ] );
matchAdjustment = 1;
}
if ( !right && match !== -1 && this._getCharType( textAhead[ match ] ) === common.CHAR_TYPE.UPPER_CASE ) {
// Detects case like moving left with thisIsFanc^y - we want to move the caret before
// uppercased letter.
match += 1;
}
if ( match !== -1 ) {
endPos = position.character + ( match * ( right ? 1 : -1 ) ) + matchAdjustment;
} else if ( !isBoundaryPosiiton ) {
// No textAhead means that the caret is in boundary position.
endPos = right ? doc.lineAt( position.line ).text.length : 0;
}
return endPos;
},
/**
* Returns lines ahead your selection, e.g. for text like:
*
* aa^aa
* bb
* cc
*
* Generator will return values: `[ 0, 'aa' ], [ 1, 'bb' ], [ 2, 'cc' ]`.
*
* **Note how for first line only text after/before caret gets returned.**
*
* @param {Position} startPosition
* @param {Boolean} [right=true] Tells the direction of generator. If `false` returned lines text is also **reversed**.
* @returns {Array} Array in form [ <lineNumber>, <lineContent> ], where lineNumber is a 0-based number.
*/
* _getAheadLines( doc, startPosition, right ) {
right = right === undefined ? true : right;
let curLine = startPosition.line,
// By how much we change line per iteration? Negative if we're going back.
lineNumberChange = right ? 1 : -1,
isLineValid = lineNumber => lineNumber >= 0 && lineNumber < doc.lineCount,
getLineText = lineNumber => {
let ret = doc.lineAt( lineNumber ).text;
if ( !right ) {
ret = reverseString( ret );
}
// First line is a special case, where we want to return only content from/to startPosition.
if ( lineNumber === startPosition.line ) {
ret = ret.substr( right ? startPosition.character : ret.length - startPosition.character );
}
return ret;
};
while ( isLineValid( curLine ) ) {
yield [
curLine,
getLineText( curLine )
];
// Set line number for further fetch.
curLine += lineNumberChange;
}
},
/**
* Tells the character type based on `char`.
*
* @param {String} char A character to be tested.
* @returns {Number} A value based on `CHAR_TYPE` members.
*/
_getCharType( char ) {
let regExpMapping = common.regExpMapping;
char = String( char )[ 0 ] || '';
// Check for unicode (#1).
if ( char.match( this._unicodeRegexp ) ) {
return char.toUpperCase() === char ? CHAR_TYPE.UPPER_CASE : CHAR_TYPE.LOWER_CASE;
}
for ( let typeValue in regExpMapping ) {
if ( char.match( regExpMapping[ typeValue ] ) ) {
return Number( typeValue );
}
}
return CHAR_TYPE.OTHER;
},
/**
* Returns currently focused text editor.
*
* @param {TextEditor/null} [editor=null] If given it will be returned instead of looking for a default one.
* @returns {TextEditor/undefined}
*/
_getEditor( textEditor ) {
return this._isTextEditor( textEditor ) ? textEditor : vscode.window.activeTextEditor;
},
/**
*
*
* @param {any} editor
* @returns {Boolean} `true` if `editor` looks like an instance of TextEditor.
*/
_isTextEditor( editor ) {
return editor && editor.document;
},
_unicodeRegexp: /[\u00C0-\u017F]/
};