SQL Operations Studio Public Preview 1 (0.23) release source code
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .bracket-match {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
233
src/vs/editor/contrib/bracketMatching/common/bracketMatching.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { editorAction, commonEditorContribution, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorBracketMatchBackground, editorBracketMatchBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations';
|
||||
|
||||
@editorAction
|
||||
class SelectBracketAction extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.jumpToBracket',
|
||||
label: nls.localize('smartSelect.jumpBracket', "Go to Bracket"),
|
||||
alias: 'Go to Bracket',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
|
||||
let controller = BracketMatchingController.get(editor);
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
controller.jumpToBracket();
|
||||
}
|
||||
}
|
||||
|
||||
type Brackets = [Range, Range];
|
||||
|
||||
class BracketsData {
|
||||
public readonly position: Position;
|
||||
public readonly brackets: Brackets;
|
||||
|
||||
constructor(position: Position, brackets: Brackets) {
|
||||
this.position = position;
|
||||
this.brackets = brackets;
|
||||
}
|
||||
}
|
||||
|
||||
@commonEditorContribution
|
||||
export class BracketMatchingController extends Disposable implements editorCommon.IEditorContribution {
|
||||
private static ID = 'editor.contrib.bracketMatchingController';
|
||||
|
||||
public static get(editor: editorCommon.ICommonCodeEditor): BracketMatchingController {
|
||||
return editor.getContribution<BracketMatchingController>(BracketMatchingController.ID);
|
||||
}
|
||||
|
||||
private readonly _editor: editorCommon.ICommonCodeEditor;
|
||||
|
||||
private _lastBracketsData: BracketsData[];
|
||||
private _lastVersionId: number;
|
||||
private _decorations: string[];
|
||||
private _updateBracketsSoon: RunOnceScheduler;
|
||||
private _matchBrackets: boolean;
|
||||
|
||||
constructor(
|
||||
editor: editorCommon.ICommonCodeEditor
|
||||
) {
|
||||
super();
|
||||
this._editor = editor;
|
||||
this._lastBracketsData = [];
|
||||
this._lastVersionId = 0;
|
||||
this._decorations = [];
|
||||
this._updateBracketsSoon = this._register(new RunOnceScheduler(() => this._updateBrackets(), 50));
|
||||
this._matchBrackets = this._editor.getConfiguration().contribInfo.matchBrackets;
|
||||
|
||||
this._updateBracketsSoon.schedule();
|
||||
this._register(editor.onDidChangeCursorPosition((e) => {
|
||||
|
||||
if (!this._matchBrackets) {
|
||||
// Early exit if nothing needs to be done!
|
||||
// Leave some form of early exit check here if you wish to continue being a cursor position change listener ;)
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateBracketsSoon.schedule();
|
||||
}));
|
||||
this._register(editor.onDidChangeModel((e) => { this._decorations = []; this._updateBracketsSoon.schedule(); }));
|
||||
this._register(editor.onDidChangeConfiguration((e) => {
|
||||
this._matchBrackets = this._editor.getConfiguration().contribInfo.matchBrackets;
|
||||
if (!this._matchBrackets && this._decorations.length > 0) {
|
||||
// Remove existing decorations if bracket matching is off
|
||||
this._decorations = this._editor.deltaDecorations(this._decorations, []);
|
||||
}
|
||||
this._updateBracketsSoon.schedule();
|
||||
}));
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return BracketMatchingController.ID;
|
||||
}
|
||||
|
||||
public jumpToBracket(): void {
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newSelections = this._editor.getSelections().map(selection => {
|
||||
const position = selection.getStartPosition();
|
||||
|
||||
// find matching brackets if position is on a bracket
|
||||
const brackets = model.matchBracket(position);
|
||||
let newCursorPosition: Position = null;
|
||||
if (brackets) {
|
||||
if (brackets[0].containsPosition(position)) {
|
||||
newCursorPosition = brackets[1].getStartPosition();
|
||||
} else if (brackets[1].containsPosition(position)) {
|
||||
newCursorPosition = brackets[0].getStartPosition();
|
||||
}
|
||||
} else {
|
||||
// find the next bracket if the position isn't on a matching bracket
|
||||
const nextBracket = model.findNextBracket(position);
|
||||
if (nextBracket && nextBracket.range) {
|
||||
newCursorPosition = nextBracket.range.getStartPosition();
|
||||
}
|
||||
}
|
||||
|
||||
if (newCursorPosition) {
|
||||
return new Selection(newCursorPosition.lineNumber, newCursorPosition.column, newCursorPosition.lineNumber, newCursorPosition.column);
|
||||
}
|
||||
return new Selection(position.lineNumber, position.column, position.lineNumber, position.column);
|
||||
});
|
||||
|
||||
this._editor.setSelections(newSelections);
|
||||
}
|
||||
|
||||
private static _DECORATION_OPTIONS = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'bracket-match'
|
||||
});
|
||||
|
||||
private _updateBrackets(): void {
|
||||
if (!this._matchBrackets) {
|
||||
return;
|
||||
}
|
||||
this._recomputeBrackets();
|
||||
|
||||
let newDecorations: editorCommon.IModelDeltaDecoration[] = [], newDecorationsLen = 0;
|
||||
for (let i = 0, len = this._lastBracketsData.length; i < len; i++) {
|
||||
let brackets = this._lastBracketsData[i].brackets;
|
||||
if (brackets) {
|
||||
newDecorations[newDecorationsLen++] = { range: brackets[0], options: BracketMatchingController._DECORATION_OPTIONS };
|
||||
newDecorations[newDecorationsLen++] = { range: brackets[1], options: BracketMatchingController._DECORATION_OPTIONS };
|
||||
}
|
||||
}
|
||||
|
||||
this._decorations = this._editor.deltaDecorations(this._decorations, newDecorations);
|
||||
}
|
||||
|
||||
private _recomputeBrackets(): void {
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
// no model => no brackets!
|
||||
this._lastBracketsData = [];
|
||||
this._lastVersionId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const versionId = model.getVersionId();
|
||||
let previousData: BracketsData[] = [];
|
||||
if (this._lastVersionId === versionId) {
|
||||
// use the previous data only if the model is at the same version id
|
||||
previousData = this._lastBracketsData;
|
||||
}
|
||||
|
||||
const selections = this._editor.getSelections();
|
||||
|
||||
let positions: Position[] = [], positionsLen = 0;
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
let selection = selections[i];
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
// will bracket match a cursor only if the selection is collapsed
|
||||
positions[positionsLen++] = selection.getStartPosition();
|
||||
}
|
||||
}
|
||||
|
||||
// sort positions for `previousData` cache hits
|
||||
if (positions.length > 1) {
|
||||
positions.sort(Position.compare);
|
||||
}
|
||||
|
||||
let newData: BracketsData[] = [], newDataLen = 0;
|
||||
let previousIndex = 0, previousLen = previousData.length;
|
||||
for (let i = 0, len = positions.length; i < len; i++) {
|
||||
let position = positions[i];
|
||||
|
||||
while (previousIndex < previousLen && previousData[previousIndex].position.isBefore(position)) {
|
||||
previousIndex++;
|
||||
}
|
||||
|
||||
if (previousIndex < previousLen && previousData[previousIndex].position.equals(position)) {
|
||||
newData[newDataLen++] = previousData[previousIndex];
|
||||
} else {
|
||||
let brackets = model.matchBracket(position);
|
||||
newData[newDataLen++] = new BracketsData(position, brackets);
|
||||
}
|
||||
}
|
||||
|
||||
this._lastBracketsData = newData;
|
||||
this._lastVersionId = versionId;
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let bracketMatchBackground = theme.getColor(editorBracketMatchBackground);
|
||||
if (bracketMatchBackground) {
|
||||
collector.addRule(`.monaco-editor .bracket-match { background-color: ${bracketMatchBackground}; }`);
|
||||
}
|
||||
let bracketMatchBorder = theme.getColor(editorBracketMatchBorder);
|
||||
if (bracketMatchBorder) {
|
||||
collector.addRule(`.monaco-editor .bracket-match { border: 1px solid ${bracketMatchBorder}; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Model } from 'vs/editor/common/model/model';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { MockMode } from 'vs/editor/test/common/mocks/mockMode';
|
||||
import { LanguageIdentifier } from 'vs/editor/common/modes';
|
||||
import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/common/bracketMatching';
|
||||
|
||||
suite('bracket matching', () => {
|
||||
class BracketMode extends MockMode {
|
||||
|
||||
private static _id = new LanguageIdentifier('bracketMode', 3);
|
||||
|
||||
constructor() {
|
||||
super(BracketMode._id);
|
||||
this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {
|
||||
brackets: [
|
||||
['{', '}'],
|
||||
['[', ']'],
|
||||
['(', ')'],
|
||||
]
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
test('issue #183: jump to matching bracket position', () => {
|
||||
let mode = new BracketMode();
|
||||
let model = Model.createFromString('var x = (3 + (5-7)) + ((5+3)+5);', undefined, mode.getLanguageIdentifier());
|
||||
|
||||
withMockCodeEditor(null, { model: model }, (editor, cursor) => {
|
||||
let bracketMatchingController = editor.registerAndInstantiateContribution<BracketMatchingController>(BracketMatchingController);
|
||||
|
||||
// start on closing bracket
|
||||
editor.setPosition(new Position(1, 20));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 9));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 19));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 9));
|
||||
|
||||
// start on opening bracket
|
||||
editor.setPosition(new Position(1, 23));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 31));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 23));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 31));
|
||||
|
||||
bracketMatchingController.dispose();
|
||||
});
|
||||
|
||||
model.dispose();
|
||||
mode.dispose();
|
||||
});
|
||||
|
||||
test('Jump to next bracket', () => {
|
||||
let mode = new BracketMode();
|
||||
let model = Model.createFromString('var x = (3 + (5-7)); y();', undefined, mode.getLanguageIdentifier());
|
||||
|
||||
withMockCodeEditor(null, { model: model }, (editor, cursor) => {
|
||||
let bracketMatchingController = editor.registerAndInstantiateContribution<BracketMatchingController>(BracketMatchingController);
|
||||
|
||||
// start position between brackets
|
||||
editor.setPosition(new Position(1, 16));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 18));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 14));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 18));
|
||||
|
||||
// skip brackets in comments
|
||||
editor.setPosition(new Position(1, 21));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 23));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 24));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 23));
|
||||
|
||||
// do not break if no brackets are available
|
||||
editor.setPosition(new Position(1, 26));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 26));
|
||||
|
||||
bracketMatchingController.dispose();
|
||||
});
|
||||
|
||||
model.dispose();
|
||||
mode.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { ICommand, ICommonCodeEditor } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { IActionOptions, editorAction, EditorAction, ServicesAccessor } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { MoveCaretCommand } from './moveCaretCommand';
|
||||
|
||||
class MoveCaretAction extends EditorAction {
|
||||
|
||||
private left: boolean;
|
||||
|
||||
constructor(left: boolean, opts: IActionOptions) {
|
||||
super(opts);
|
||||
|
||||
this.left = left;
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
|
||||
|
||||
var commands: ICommand[] = [];
|
||||
var selections = editor.getSelections();
|
||||
|
||||
for (var i = 0; i < selections.length; i++) {
|
||||
commands.push(new MoveCaretCommand(selections[i], this.left));
|
||||
}
|
||||
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, commands);
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class MoveCaretLeftAction extends MoveCaretAction {
|
||||
constructor() {
|
||||
super(true, {
|
||||
id: 'editor.action.moveCarretLeftAction',
|
||||
label: nls.localize('caret.moveLeft', "Move Caret Left"),
|
||||
alias: 'Move Caret Left',
|
||||
precondition: EditorContextKeys.writable
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class MoveCaretRightAction extends MoveCaretAction {
|
||||
constructor() {
|
||||
super(false, {
|
||||
id: 'editor.action.moveCarretRightAction',
|
||||
label: nls.localize('caret.moveRight', "Move Caret Right"),
|
||||
alias: 'Move Caret Right',
|
||||
precondition: EditorContextKeys.writable
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ICommand, ICursorStateComputerData, IEditOperationBuilder, ITokenizedModel } from 'vs/editor/common/editorCommon';
|
||||
|
||||
export class MoveCaretCommand implements ICommand {
|
||||
|
||||
private _selection: Selection;
|
||||
private _isMovingLeft: boolean;
|
||||
|
||||
private _cutStartIndex: number;
|
||||
private _cutEndIndex: number;
|
||||
private _moved: boolean;
|
||||
|
||||
private _selectionId: string;
|
||||
|
||||
constructor(selection: Selection, isMovingLeft: boolean) {
|
||||
this._selection = selection;
|
||||
this._isMovingLeft = isMovingLeft;
|
||||
}
|
||||
|
||||
public getEditOperations(model: ITokenizedModel, builder: IEditOperationBuilder): void {
|
||||
var s = this._selection;
|
||||
this._selectionId = builder.trackSelection(s);
|
||||
if (s.startLineNumber !== s.endLineNumber) {
|
||||
return;
|
||||
}
|
||||
if (this._isMovingLeft && s.startColumn === 0) {
|
||||
return;
|
||||
} else if (!this._isMovingLeft && s.endColumn === model.getLineMaxColumn(s.startLineNumber)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var lineNumber = s.selectionStartLineNumber;
|
||||
var lineContent = model.getLineContent(lineNumber);
|
||||
|
||||
var left;
|
||||
var middle;
|
||||
var right;
|
||||
|
||||
if (this._isMovingLeft) {
|
||||
left = lineContent.substring(0, s.startColumn - 2);
|
||||
middle = lineContent.substring(s.startColumn - 1, s.endColumn - 1);
|
||||
right = lineContent.substring(s.startColumn - 2, s.startColumn - 1) + lineContent.substring(s.endColumn - 1);
|
||||
} else {
|
||||
left = lineContent.substring(0, s.startColumn - 1) + lineContent.substring(s.endColumn - 1, s.endColumn);
|
||||
middle = lineContent.substring(s.startColumn - 1, s.endColumn - 1);
|
||||
right = lineContent.substring(s.endColumn);
|
||||
}
|
||||
|
||||
var newLineContent = left + middle + right;
|
||||
|
||||
builder.addEditOperation(new Range(lineNumber, 1, lineNumber, model.getLineMaxColumn(lineNumber)), null);
|
||||
builder.addEditOperation(new Range(lineNumber, 1, lineNumber, 1), newLineContent);
|
||||
|
||||
this._cutStartIndex = s.startColumn + (this._isMovingLeft ? -1 : 1);
|
||||
this._cutEndIndex = this._cutStartIndex + s.endColumn - s.startColumn;
|
||||
this._moved = true;
|
||||
}
|
||||
|
||||
public computeCursorState(model: ITokenizedModel, helper: ICursorStateComputerData): Selection {
|
||||
var result = helper.getTrackedSelection(this._selectionId);
|
||||
if (this._moved) {
|
||||
result = result.setStartPosition(result.startLineNumber, this._cutStartIndex);
|
||||
result = result.setEndPosition(result.startLineNumber, this._cutEndIndex);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
71
src/vs/editor/contrib/caretOperations/common/transpose.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ICommand, ICommonCodeEditor } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { editorAction, EditorAction, ServicesAccessor } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand';
|
||||
|
||||
@editorAction
|
||||
class TransposeLettersAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.transposeLetters',
|
||||
label: nls.localize('transposeLetters.label', "Transpose Letters"),
|
||||
alias: 'Transpose Letters',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: 0,
|
||||
mac: {
|
||||
primary: KeyMod.WinCtrl | KeyCode.KEY_T
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
|
||||
let model = editor.getModel();
|
||||
let commands: ICommand[] = [];
|
||||
let selections = editor.getSelections();
|
||||
|
||||
for (let i = 0; i < selections.length; i++) {
|
||||
let selection = selections[i];
|
||||
if (!selection.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
let lineNumber = selection.startLineNumber;
|
||||
let column = selection.startColumn;
|
||||
if (column === 1) {
|
||||
// at the beginning of line
|
||||
continue;
|
||||
}
|
||||
let maxColumn = model.getLineMaxColumn(lineNumber);
|
||||
if (column === maxColumn) {
|
||||
// at the end of line
|
||||
continue;
|
||||
}
|
||||
|
||||
let lineContent = model.getLineContent(lineNumber);
|
||||
let charToTheLeft = lineContent.charAt(column - 2);
|
||||
let charToTheRight = lineContent.charAt(column - 1);
|
||||
|
||||
let replaceRange = new Range(lineNumber, column - 1, lineNumber, column + 1);
|
||||
|
||||
commands.push(new ReplaceCommand(replaceRange, charToTheRight + charToTheLeft));
|
||||
}
|
||||
|
||||
if (commands.length > 0) {
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, commands);
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { MoveCaretCommand } from 'vs/editor/contrib/caretOperations/common/moveCaretCommand';
|
||||
import { testCommand } from 'vs/editor/test/common/commands/commandTestUtils';
|
||||
|
||||
|
||||
function testMoveCaretLeftCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void {
|
||||
testCommand(lines, null, selection, (sel) => new MoveCaretCommand(sel, true), expectedLines, expectedSelection);
|
||||
}
|
||||
|
||||
function testMoveCaretRightCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void {
|
||||
testCommand(lines, null, selection, (sel) => new MoveCaretCommand(sel, false), expectedLines, expectedSelection);
|
||||
}
|
||||
|
||||
suite('Editor Contrib - Move Caret Command', () => {
|
||||
|
||||
test('move selection to left', function () {
|
||||
testMoveCaretLeftCommand(
|
||||
[
|
||||
'012345'
|
||||
],
|
||||
new Selection(1, 3, 1, 5),
|
||||
[
|
||||
'023145'
|
||||
],
|
||||
new Selection(1, 2, 1, 4)
|
||||
);
|
||||
});
|
||||
test('move selection to right', function () {
|
||||
testMoveCaretRightCommand(
|
||||
[
|
||||
'012345'
|
||||
],
|
||||
new Selection(1, 3, 1, 5),
|
||||
[
|
||||
'014235'
|
||||
],
|
||||
new Selection(1, 4, 1, 6)
|
||||
);
|
||||
});
|
||||
test('move selection to left - from first column - no change', function () {
|
||||
testMoveCaretLeftCommand(
|
||||
[
|
||||
'012345'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
'012345'
|
||||
],
|
||||
new Selection(1, 1, 1, 1)
|
||||
);
|
||||
});
|
||||
test('move selection to right - from last column - no change', function () {
|
||||
testMoveCaretRightCommand(
|
||||
[
|
||||
'012345'
|
||||
],
|
||||
new Selection(1, 5, 1, 7),
|
||||
[
|
||||
'012345'
|
||||
],
|
||||
new Selection(1, 5, 1, 7)
|
||||
);
|
||||
});
|
||||
});
|
||||
8
src/vs/editor/contrib/clipboard/browser/clipboard.css
Normal file
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-menu .monaco-action-bar.vertical .action-label.hover {
|
||||
background-color: #EEE;
|
||||
}
|
||||
199
src/vs/editor/contrib/clipboard/browser/clipboard.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./clipboard';
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { editorAction, IActionOptions, EditorAction, ICommandKeybindingsOptions } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { CopyOptions } from 'vs/editor/browser/controller/textAreaInput';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
|
||||
const CLIPBOARD_CONTEXT_MENU_GROUP = '9_cutcopypaste';
|
||||
|
||||
const supportsCut = (platform.isNative || document.queryCommandSupported('cut'));
|
||||
const supportsCopy = (platform.isNative || document.queryCommandSupported('copy'));
|
||||
// IE and Edge have trouble with setting html content in clipboard
|
||||
const supportsCopyWithSyntaxHighlighting = (supportsCopy && !browser.isEdgeOrIE);
|
||||
// Chrome incorrectly returns true for document.queryCommandSupported('paste')
|
||||
// when the paste feature is available but the calling script has insufficient
|
||||
// privileges to actually perform the action
|
||||
const supportsPaste = (platform.isNative || (!browser.isChrome && document.queryCommandSupported('paste')));
|
||||
|
||||
type ExecCommand = 'cut' | 'copy' | 'paste';
|
||||
|
||||
function conditionalEditorAction(condition: boolean) {
|
||||
if (!condition) {
|
||||
return () => { };
|
||||
}
|
||||
return editorAction;
|
||||
}
|
||||
|
||||
abstract class ExecCommandAction extends EditorAction {
|
||||
|
||||
private browserCommand: ExecCommand;
|
||||
|
||||
constructor(browserCommand: ExecCommand, opts: IActionOptions) {
|
||||
super(opts);
|
||||
this.browserCommand = browserCommand;
|
||||
}
|
||||
|
||||
public runCommand(accessor: ServicesAccessor, args: any): void {
|
||||
let focusedEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
|
||||
// Only if editor text focus (i.e. not if editor has widget focus).
|
||||
if (focusedEditor && focusedEditor.isFocused()) {
|
||||
focusedEditor.trigger('keyboard', this.id, args);
|
||||
return;
|
||||
}
|
||||
|
||||
document.execCommand(this.browserCommand);
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
|
||||
editor.focus();
|
||||
document.execCommand(this.browserCommand);
|
||||
}
|
||||
}
|
||||
|
||||
@conditionalEditorAction(supportsCut)
|
||||
class ExecCommandCutAction extends ExecCommandAction {
|
||||
|
||||
constructor() {
|
||||
let kbOpts: ICommandKeybindingsOptions = {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_X,
|
||||
win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_X, secondary: [KeyMod.Shift | KeyCode.Delete] }
|
||||
};
|
||||
// Do not bind cut keybindings in the browser,
|
||||
// since browsers do that for us and it avoids security prompts
|
||||
if (!platform.isNative) {
|
||||
kbOpts = null;
|
||||
}
|
||||
super('cut', {
|
||||
id: 'editor.action.clipboardCutAction',
|
||||
label: nls.localize('actions.clipboard.cutLabel', "Cut"),
|
||||
alias: 'Cut',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: kbOpts,
|
||||
menuOpts: {
|
||||
group: CLIPBOARD_CONTEXT_MENU_GROUP,
|
||||
order: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
|
||||
const emptySelectionClipboard = editor.getConfiguration().emptySelectionClipboard;
|
||||
|
||||
if (!emptySelectionClipboard && editor.getSelection().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.run(accessor, editor);
|
||||
}
|
||||
}
|
||||
|
||||
@conditionalEditorAction(supportsCopy)
|
||||
class ExecCommandCopyAction extends ExecCommandAction {
|
||||
|
||||
constructor() {
|
||||
let kbOpts: ICommandKeybindingsOptions = {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
|
||||
win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, secondary: [KeyMod.CtrlCmd | KeyCode.Insert] }
|
||||
};
|
||||
// Do not bind copy keybindings in the browser,
|
||||
// since browsers do that for us and it avoids security prompts
|
||||
if (!platform.isNative) {
|
||||
kbOpts = null;
|
||||
}
|
||||
|
||||
super('copy', {
|
||||
id: 'editor.action.clipboardCopyAction',
|
||||
label: nls.localize('actions.clipboard.copyLabel', "Copy"),
|
||||
alias: 'Copy',
|
||||
precondition: null,
|
||||
kbOpts: kbOpts,
|
||||
menuOpts: {
|
||||
group: CLIPBOARD_CONTEXT_MENU_GROUP,
|
||||
order: 2
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
|
||||
const emptySelectionClipboard = editor.getConfiguration().emptySelectionClipboard;
|
||||
|
||||
if (!emptySelectionClipboard && editor.getSelection().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.run(accessor, editor);
|
||||
}
|
||||
}
|
||||
|
||||
@conditionalEditorAction(supportsPaste)
|
||||
class ExecCommandPasteAction extends ExecCommandAction {
|
||||
|
||||
constructor() {
|
||||
let kbOpts: ICommandKeybindingsOptions = {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
|
||||
win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.Shift | KeyCode.Insert] }
|
||||
};
|
||||
// Do not bind paste keybindings in the browser,
|
||||
// since browsers do that for us and it avoids security prompts
|
||||
if (!platform.isNative) {
|
||||
kbOpts = null;
|
||||
}
|
||||
|
||||
super('paste', {
|
||||
id: 'editor.action.clipboardPasteAction',
|
||||
label: nls.localize('actions.clipboard.pasteLabel', "Paste"),
|
||||
alias: 'Paste',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: kbOpts,
|
||||
menuOpts: {
|
||||
group: CLIPBOARD_CONTEXT_MENU_GROUP,
|
||||
order: 3
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@conditionalEditorAction(supportsCopyWithSyntaxHighlighting)
|
||||
class ExecCommandCopyWithSyntaxHighlightingAction extends ExecCommandAction {
|
||||
|
||||
constructor() {
|
||||
super('copy', {
|
||||
id: 'editor.action.clipboardCopyWithSyntaxHighlightingAction',
|
||||
label: nls.localize('actions.clipboard.copyWithSyntaxHighlightingLabel', "Copy With Syntax Highlighting"),
|
||||
alias: 'Copy With Syntax Highlighting',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: null
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
|
||||
const emptySelectionClipboard = editor.getConfiguration().emptySelectionClipboard;
|
||||
|
||||
if (!emptySelectionClipboard && editor.getSelection().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
CopyOptions.forceCopyWithSyntaxHighlighting = true;
|
||||
super.run(accessor, editor);
|
||||
CopyOptions.forceCopyWithSyntaxHighlighting = false;
|
||||
}
|
||||
}
|
||||
72
src/vs/editor/contrib/codelens/browser/codelens.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { mergeSort } from 'vs/base/common/arrays';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IModel } from 'vs/editor/common/editorCommon';
|
||||
import { CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { CodeLensProviderRegistry, CodeLensProvider, ICodeLensSymbol } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { asWinJsPromise } from 'vs/base/common/async';
|
||||
|
||||
export interface ICodeLensData {
|
||||
symbol: ICodeLensSymbol;
|
||||
provider: CodeLensProvider;
|
||||
}
|
||||
|
||||
export function getCodeLensData(model: IModel): TPromise<ICodeLensData[]> {
|
||||
|
||||
const symbols: ICodeLensData[] = [];
|
||||
const provider = CodeLensProviderRegistry.ordered(model);
|
||||
|
||||
const promises = provider.map(provider => asWinJsPromise(token => provider.provideCodeLenses(model, token)).then(result => {
|
||||
if (Array.isArray(result)) {
|
||||
for (let symbol of result) {
|
||||
symbols.push({ symbol, provider });
|
||||
}
|
||||
}
|
||||
}, onUnexpectedExternalError));
|
||||
|
||||
return TPromise.join(promises).then(() => {
|
||||
|
||||
return mergeSort(symbols, (a, b) => {
|
||||
// sort by lineNumber, provider-rank, and column
|
||||
if (a.symbol.range.startLineNumber < b.symbol.range.startLineNumber) {
|
||||
return -1;
|
||||
} else if (a.symbol.range.startLineNumber > b.symbol.range.startLineNumber) {
|
||||
return 1;
|
||||
} else if (provider.indexOf(a.provider) < provider.indexOf(b.provider)) {
|
||||
return -1;
|
||||
} else if (provider.indexOf(a.provider) > provider.indexOf(b.provider)) {
|
||||
return 1;
|
||||
} else if (a.symbol.range.startColumn < b.symbol.range.startColumn) {
|
||||
return -1;
|
||||
} else if (a.symbol.range.startColumn > b.symbol.range.startColumn) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
CommonEditorRegistry.registerLanguageCommand('_executeCodeLensProvider', function (accessor, args) {
|
||||
|
||||
const { resource } = args;
|
||||
if (!(resource instanceof URI)) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
return getCodeLensData(model).then(value => value.map(item => item.symbol));
|
||||
});
|
||||
306
src/vs/editor/contrib/codelens/browser/codelensController.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { RunOnceScheduler, asWinJsPromise } from 'vs/base/common/async';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IMessageService } from 'vs/platform/message/common/message';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { CodeLensProviderRegistry, ICodeLensSymbol } from 'vs/editor/common/modes';
|
||||
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
import { ICodeLensData, getCodeLensData } from './codelens';
|
||||
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
|
||||
import { CodeLens, CodeLensHelper } from 'vs/editor/contrib/codelens/browser/codelensWidget';
|
||||
|
||||
@editorContribution
|
||||
export class CodeLensContribution implements editorCommon.IEditorContribution {
|
||||
|
||||
private static ID: string = 'css.editor.codeLens';
|
||||
|
||||
private _isEnabled: boolean;
|
||||
|
||||
private _globalToDispose: IDisposable[];
|
||||
private _localToDispose: IDisposable[];
|
||||
private _lenses: CodeLens[];
|
||||
private _currentFindCodeLensSymbolsPromise: TPromise<ICodeLensData[]>;
|
||||
private _modelChangeCounter: number;
|
||||
private _currentFindOccPromise: TPromise<any>;
|
||||
private _detectVisibleLenses: RunOnceScheduler;
|
||||
|
||||
constructor(
|
||||
private _editor: editorBrowser.ICodeEditor,
|
||||
@ICommandService private _commandService: ICommandService,
|
||||
@IMessageService private _messageService: IMessageService
|
||||
) {
|
||||
this._isEnabled = this._editor.getConfiguration().contribInfo.codeLens;
|
||||
|
||||
this._globalToDispose = [];
|
||||
this._localToDispose = [];
|
||||
this._lenses = [];
|
||||
this._currentFindCodeLensSymbolsPromise = null;
|
||||
this._modelChangeCounter = 0;
|
||||
|
||||
this._globalToDispose.push(this._editor.onDidChangeModel(() => this._onModelChange()));
|
||||
this._globalToDispose.push(this._editor.onDidChangeModelLanguage(() => this._onModelChange()));
|
||||
this._globalToDispose.push(this._editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
|
||||
let prevIsEnabled = this._isEnabled;
|
||||
this._isEnabled = this._editor.getConfiguration().contribInfo.codeLens;
|
||||
if (prevIsEnabled !== this._isEnabled) {
|
||||
this._onModelChange();
|
||||
}
|
||||
}));
|
||||
this._globalToDispose.push(CodeLensProviderRegistry.onDidChange(this._onModelChange, this));
|
||||
this._onModelChange();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._localDispose();
|
||||
this._globalToDispose = dispose(this._globalToDispose);
|
||||
}
|
||||
|
||||
private _localDispose(): void {
|
||||
if (this._currentFindCodeLensSymbolsPromise) {
|
||||
this._currentFindCodeLensSymbolsPromise.cancel();
|
||||
this._currentFindCodeLensSymbolsPromise = null;
|
||||
this._modelChangeCounter++;
|
||||
}
|
||||
if (this._currentFindOccPromise) {
|
||||
this._currentFindOccPromise.cancel();
|
||||
this._currentFindOccPromise = null;
|
||||
}
|
||||
this._localToDispose = dispose(this._localToDispose);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return CodeLensContribution.ID;
|
||||
}
|
||||
|
||||
private _onModelChange(): void {
|
||||
|
||||
this._localDispose();
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CodeLensProviderRegistry.has(model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const provider of CodeLensProviderRegistry.all(model)) {
|
||||
if (typeof provider.onDidChange === 'function') {
|
||||
let registration = provider.onDidChange(() => scheduler.schedule());
|
||||
this._localToDispose.push(registration);
|
||||
}
|
||||
}
|
||||
|
||||
this._detectVisibleLenses = new RunOnceScheduler(() => {
|
||||
this._onViewportChanged();
|
||||
}, 500);
|
||||
|
||||
const scheduler = new RunOnceScheduler(() => {
|
||||
const counterValue = ++this._modelChangeCounter;
|
||||
if (this._currentFindCodeLensSymbolsPromise) {
|
||||
this._currentFindCodeLensSymbolsPromise.cancel();
|
||||
}
|
||||
|
||||
this._currentFindCodeLensSymbolsPromise = getCodeLensData(model);
|
||||
|
||||
this._currentFindCodeLensSymbolsPromise.then((result) => {
|
||||
if (counterValue === this._modelChangeCounter) { // only the last one wins
|
||||
this._renderCodeLensSymbols(result);
|
||||
this._detectVisibleLenses.schedule();
|
||||
}
|
||||
}, onUnexpectedError);
|
||||
}, 250);
|
||||
this._localToDispose.push(scheduler);
|
||||
this._localToDispose.push(this._detectVisibleLenses);
|
||||
this._localToDispose.push(this._editor.onDidChangeModelContent((e) => {
|
||||
this._editor.changeDecorations((changeAccessor) => {
|
||||
this._editor.changeViewZones((viewAccessor) => {
|
||||
const toDispose: CodeLens[] = [];
|
||||
this._lenses.forEach((lens) => {
|
||||
if (lens.isValid()) {
|
||||
lens.update(viewAccessor);
|
||||
} else {
|
||||
toDispose.push(lens);
|
||||
}
|
||||
});
|
||||
|
||||
let helper = new CodeLensHelper();
|
||||
toDispose.forEach((l) => {
|
||||
l.dispose(helper, viewAccessor);
|
||||
this._lenses.splice(this._lenses.indexOf(l), 1);
|
||||
});
|
||||
helper.commit(changeAccessor);
|
||||
});
|
||||
});
|
||||
|
||||
// Compute new `visible` code lenses
|
||||
this._detectVisibleLenses.schedule();
|
||||
// Ask for all references again
|
||||
scheduler.schedule();
|
||||
}));
|
||||
this._localToDispose.push(this._editor.onDidScrollChange(e => {
|
||||
if (e.scrollTopChanged && this._lenses.length > 0) {
|
||||
this._detectVisibleLenses.schedule();
|
||||
}
|
||||
}));
|
||||
this._localToDispose.push(this._editor.onDidLayoutChange(e => {
|
||||
this._detectVisibleLenses.schedule();
|
||||
}));
|
||||
this._localToDispose.push({
|
||||
dispose: () => {
|
||||
if (this._editor.getModel()) {
|
||||
this._editor.changeDecorations((changeAccessor) => {
|
||||
this._editor.changeViewZones((accessor) => {
|
||||
this._disposeAllLenses(changeAccessor, accessor);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// No accessors available
|
||||
this._disposeAllLenses(null, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scheduler.schedule();
|
||||
}
|
||||
|
||||
private _disposeAllLenses(decChangeAccessor: editorCommon.IModelDecorationsChangeAccessor, viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor): void {
|
||||
let helper = new CodeLensHelper();
|
||||
this._lenses.forEach((lens) => lens.dispose(helper, viewZoneChangeAccessor));
|
||||
if (decChangeAccessor) {
|
||||
helper.commit(decChangeAccessor);
|
||||
}
|
||||
this._lenses = [];
|
||||
}
|
||||
|
||||
private _renderCodeLensSymbols(symbols: ICodeLensData[]): void {
|
||||
if (!this._editor.getModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let maxLineNumber = this._editor.getModel().getLineCount();
|
||||
let groups: ICodeLensData[][] = [];
|
||||
let lastGroup: ICodeLensData[];
|
||||
|
||||
for (let symbol of symbols) {
|
||||
let line = symbol.symbol.range.startLineNumber;
|
||||
if (line < 1 || line > maxLineNumber) {
|
||||
// invalid code lens
|
||||
continue;
|
||||
} else if (lastGroup && lastGroup[lastGroup.length - 1].symbol.range.startLineNumber === line) {
|
||||
// on same line as previous
|
||||
lastGroup.push(symbol);
|
||||
} else {
|
||||
// on later line as previous
|
||||
lastGroup = [symbol];
|
||||
groups.push(lastGroup);
|
||||
}
|
||||
}
|
||||
|
||||
const centeredRange = this._editor.getCenteredRangeInViewport();
|
||||
const shouldRestoreCenteredRange = centeredRange && (groups.length !== this._lenses.length && this._editor.getScrollTop() !== 0);
|
||||
this._editor.changeDecorations((changeAccessor) => {
|
||||
this._editor.changeViewZones((accessor) => {
|
||||
|
||||
let codeLensIndex = 0, groupsIndex = 0, helper = new CodeLensHelper();
|
||||
|
||||
while (groupsIndex < groups.length && codeLensIndex < this._lenses.length) {
|
||||
|
||||
let symbolsLineNumber = groups[groupsIndex][0].symbol.range.startLineNumber;
|
||||
let codeLensLineNumber = this._lenses[codeLensIndex].getLineNumber();
|
||||
|
||||
if (codeLensLineNumber < symbolsLineNumber) {
|
||||
this._lenses[codeLensIndex].dispose(helper, accessor);
|
||||
this._lenses.splice(codeLensIndex, 1);
|
||||
} else if (codeLensLineNumber === symbolsLineNumber) {
|
||||
this._lenses[codeLensIndex].updateCodeLensSymbols(groups[groupsIndex], helper);
|
||||
groupsIndex++;
|
||||
codeLensIndex++;
|
||||
} else {
|
||||
this._lenses.splice(codeLensIndex, 0, new CodeLens(groups[groupsIndex], this._editor, helper, accessor, this._commandService, this._messageService, () => this._detectVisibleLenses.schedule()));
|
||||
codeLensIndex++;
|
||||
groupsIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete extra code lenses
|
||||
while (codeLensIndex < this._lenses.length) {
|
||||
this._lenses[codeLensIndex].dispose(helper, accessor);
|
||||
this._lenses.splice(codeLensIndex, 1);
|
||||
}
|
||||
|
||||
// Create extra symbols
|
||||
while (groupsIndex < groups.length) {
|
||||
this._lenses.push(new CodeLens(groups[groupsIndex], this._editor, helper, accessor, this._commandService, this._messageService, () => this._detectVisibleLenses.schedule()));
|
||||
groupsIndex++;
|
||||
}
|
||||
|
||||
helper.commit(changeAccessor);
|
||||
});
|
||||
});
|
||||
if (shouldRestoreCenteredRange) {
|
||||
this._editor.revealRangeInCenter(centeredRange, editorCommon.ScrollType.Immediate);
|
||||
}
|
||||
}
|
||||
|
||||
private _onViewportChanged(): void {
|
||||
if (this._currentFindOccPromise) {
|
||||
this._currentFindOccPromise.cancel();
|
||||
this._currentFindOccPromise = null;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toResolve: ICodeLensData[][] = [];
|
||||
const lenses: CodeLens[] = [];
|
||||
this._lenses.forEach((lens) => {
|
||||
const request = lens.computeIfNecessary(model);
|
||||
if (request) {
|
||||
toResolve.push(request);
|
||||
lenses.push(lens);
|
||||
}
|
||||
});
|
||||
|
||||
if (toResolve.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = toResolve.map((request, i) => {
|
||||
|
||||
const resolvedSymbols = new Array<ICodeLensSymbol>(request.length);
|
||||
const promises = request.map((request, i) => {
|
||||
return asWinJsPromise((token) => {
|
||||
return request.provider.resolveCodeLens(model, request.symbol, token);
|
||||
}).then(symbol => {
|
||||
resolvedSymbols[i] = symbol;
|
||||
});
|
||||
});
|
||||
|
||||
return TPromise.join(promises).then(() => {
|
||||
lenses[i].updateCommands(resolvedSymbols);
|
||||
});
|
||||
});
|
||||
|
||||
this._currentFindOccPromise = TPromise.join(promises).then(() => {
|
||||
this._currentFindOccPromise = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
45
src/vs/editor/contrib/codelens/browser/codelensWidget.css
Normal file
@@ -0,0 +1,45 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .codelens-decoration {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > span,
|
||||
.monaco-editor .codelens-decoration > a {
|
||||
-moz-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > a:hover {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration.invisible-cl {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
|
||||
@-moz-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
|
||||
@-o-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
|
||||
@-webkit-keyframes fadein { 0% { opacity:0; visibility:visible;} 100% { opacity:1; } }
|
||||
|
||||
.monaco-editor .codelens-decoration.fadein {
|
||||
-webkit-animation: fadein 0.5s linear;
|
||||
-moz-animation: fadein 0.5s linear;
|
||||
-o-animation: fadein 0.5s linear;
|
||||
animation: fadein 0.5s linear;
|
||||
}
|
||||
339
src/vs/editor/contrib/codelens/browser/codelensWidget.ts
Normal file
@@ -0,0 +1,339 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./codelensWidget';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { format, escape } from 'vs/base/common/strings';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IMessageService } from 'vs/platform/message/common/message';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { ICodeLensSymbol, Command } from 'vs/editor/common/modes';
|
||||
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
|
||||
import { ICodeLensData } from './codelens';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations';
|
||||
import { editorCodeLensForeground } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
class CodeLensViewZone implements editorBrowser.IViewZone {
|
||||
|
||||
readonly heightInLines: number;
|
||||
readonly suppressMouseDown: boolean;
|
||||
readonly domNode: HTMLElement;
|
||||
|
||||
afterLineNumber: number;
|
||||
|
||||
private _lastHeight: number;
|
||||
private _onHeight: Function;
|
||||
|
||||
constructor(afterLineNumber: number, onHeight: Function) {
|
||||
this.afterLineNumber = afterLineNumber;
|
||||
this._onHeight = onHeight;
|
||||
|
||||
this.heightInLines = 1;
|
||||
this.suppressMouseDown = true;
|
||||
this.domNode = document.createElement('div');
|
||||
}
|
||||
|
||||
onComputedHeight(height: number): void {
|
||||
if (this._lastHeight === undefined) {
|
||||
this._lastHeight = height;
|
||||
} else if (this._lastHeight !== height) {
|
||||
this._lastHeight = height;
|
||||
this._onHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CodeLensContentWidget implements editorBrowser.IContentWidget {
|
||||
|
||||
private static _idPool: number = 0;
|
||||
|
||||
// Editor.IContentWidget.allowEditorOverflow
|
||||
readonly allowEditorOverflow: boolean = false;
|
||||
readonly suppressMouseDown: boolean = true;
|
||||
|
||||
private readonly _id: string;
|
||||
private readonly _domNode: HTMLElement;
|
||||
private readonly _disposables: IDisposable[] = [];
|
||||
private readonly _editor: editorBrowser.ICodeEditor;
|
||||
|
||||
private _symbolRange: Range;
|
||||
private _widgetPosition: editorBrowser.IContentWidgetPosition;
|
||||
private _commands: { [id: string]: Command } = Object.create(null);
|
||||
|
||||
constructor(
|
||||
editor: editorBrowser.ICodeEditor,
|
||||
symbolRange: Range,
|
||||
commandService: ICommandService,
|
||||
messageService: IMessageService
|
||||
) {
|
||||
|
||||
this._id = 'codeLensWidget' + (++CodeLensContentWidget._idPool);
|
||||
this._editor = editor;
|
||||
|
||||
this.setSymbolRange(symbolRange);
|
||||
|
||||
this._domNode = document.createElement('span');
|
||||
this._domNode.innerHTML = ' ';
|
||||
dom.addClass(this._domNode, 'codelens-decoration');
|
||||
dom.addClass(this._domNode, 'invisible-cl');
|
||||
this._updateHeight();
|
||||
|
||||
this._disposables.push(this._editor.onDidChangeConfiguration(e => e.fontInfo && this._updateHeight()));
|
||||
|
||||
this._disposables.push(dom.addDisposableListener(this._domNode, 'click', e => {
|
||||
let element = <HTMLElement>e.target;
|
||||
if (element.tagName === 'A' && element.id) {
|
||||
let command = this._commands[element.id];
|
||||
if (command) {
|
||||
editor.focus();
|
||||
commandService.executeCommand(command.id, ...command.arguments).done(undefined, err => {
|
||||
messageService.show(Severity.Error, err);
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._disposables);
|
||||
this._symbolRange = null;
|
||||
}
|
||||
|
||||
private _updateHeight(): void {
|
||||
const { fontInfo, lineHeight } = this._editor.getConfiguration();
|
||||
this._domNode.style.height = `${Math.round(lineHeight * 1.1)}px`;
|
||||
this._domNode.style.lineHeight = `${lineHeight}px`;
|
||||
this._domNode.style.fontSize = `${Math.round(fontInfo.fontSize * .9)}px`;
|
||||
this._domNode.innerHTML = ' ';
|
||||
}
|
||||
|
||||
updateVisibility(): void {
|
||||
if (this.isVisible()) {
|
||||
dom.removeClass(this._domNode, 'invisible-cl');
|
||||
dom.addClass(this._domNode, 'fadein');
|
||||
}
|
||||
}
|
||||
|
||||
withCommands(symbols: ICodeLensSymbol[]): void {
|
||||
this._commands = Object.create(null);
|
||||
if (!symbols || !symbols.length) {
|
||||
this._domNode.innerHTML = 'no commands';
|
||||
return;
|
||||
}
|
||||
|
||||
let html: string[] = [];
|
||||
for (let i = 0; i < symbols.length; i++) {
|
||||
let command = symbols[i].command;
|
||||
let title = escape(command.title);
|
||||
let part: string;
|
||||
if (command.id) {
|
||||
part = format('<a id={0}>{1}</a>', i, title);
|
||||
this._commands[i] = command;
|
||||
} else {
|
||||
part = format('<span>{0}</span>', title);
|
||||
}
|
||||
html.push(part);
|
||||
}
|
||||
|
||||
this._domNode.innerHTML = html.join('<span> | </span>');
|
||||
this._editor.layoutContentWidget(this);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
setSymbolRange(range: Range): void {
|
||||
this._symbolRange = range;
|
||||
|
||||
const lineNumber = range.startLineNumber;
|
||||
const column = this._editor.getModel().getLineFirstNonWhitespaceColumn(lineNumber);
|
||||
this._widgetPosition = {
|
||||
position: { lineNumber: lineNumber, column: column },
|
||||
preference: [editorBrowser.ContentWidgetPositionPreference.ABOVE]
|
||||
};
|
||||
}
|
||||
|
||||
getPosition(): editorBrowser.IContentWidgetPosition {
|
||||
return this._widgetPosition;
|
||||
}
|
||||
|
||||
isVisible(): boolean {
|
||||
return this._domNode.hasAttribute('monaco-visible-content-widget');
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDecorationIdCallback {
|
||||
(decorationId: string): void;
|
||||
}
|
||||
|
||||
export class CodeLensHelper {
|
||||
|
||||
private _removeDecorations: string[];
|
||||
private _addDecorations: editorCommon.IModelDeltaDecoration[];
|
||||
private _addDecorationsCallbacks: IDecorationIdCallback[];
|
||||
|
||||
constructor() {
|
||||
this._removeDecorations = [];
|
||||
this._addDecorations = [];
|
||||
this._addDecorationsCallbacks = [];
|
||||
}
|
||||
|
||||
addDecoration(decoration: editorCommon.IModelDeltaDecoration, callback: IDecorationIdCallback): void {
|
||||
this._addDecorations.push(decoration);
|
||||
this._addDecorationsCallbacks.push(callback);
|
||||
}
|
||||
|
||||
removeDecoration(decorationId: string): void {
|
||||
this._removeDecorations.push(decorationId);
|
||||
}
|
||||
|
||||
commit(changeAccessor: editorCommon.IModelDecorationsChangeAccessor): void {
|
||||
var resultingDecorations = changeAccessor.deltaDecorations(this._removeDecorations, this._addDecorations);
|
||||
for (let i = 0, len = resultingDecorations.length; i < len; i++) {
|
||||
this._addDecorationsCallbacks[i](resultingDecorations[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CodeLens {
|
||||
|
||||
private readonly _editor: editorBrowser.ICodeEditor;
|
||||
private readonly _viewZone: CodeLensViewZone;
|
||||
private readonly _viewZoneId: number;
|
||||
private readonly _contentWidget: CodeLensContentWidget;
|
||||
private _decorationIds: string[];
|
||||
private _data: ICodeLensData[];
|
||||
|
||||
constructor(
|
||||
data: ICodeLensData[],
|
||||
editor: editorBrowser.ICodeEditor,
|
||||
helper: CodeLensHelper,
|
||||
viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor,
|
||||
commandService: ICommandService, messageService: IMessageService,
|
||||
updateCallabck: Function
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._data = data;
|
||||
this._decorationIds = new Array<string>(this._data.length);
|
||||
|
||||
let range: Range;
|
||||
this._data.forEach((codeLensData, i) => {
|
||||
|
||||
helper.addDecoration({
|
||||
range: codeLensData.symbol.range,
|
||||
options: ModelDecorationOptions.EMPTY
|
||||
}, id => this._decorationIds[i] = id);
|
||||
|
||||
// the range contains all lenses on this line
|
||||
if (!range) {
|
||||
range = Range.lift(codeLensData.symbol.range);
|
||||
} else {
|
||||
range = Range.plusRange(range, codeLensData.symbol.range);
|
||||
}
|
||||
});
|
||||
|
||||
this._contentWidget = new CodeLensContentWidget(editor, range, commandService, messageService);
|
||||
this._viewZone = new CodeLensViewZone(range.startLineNumber - 1, updateCallabck);
|
||||
|
||||
this._viewZoneId = viewZoneChangeAccessor.addZone(this._viewZone);
|
||||
this._editor.addContentWidget(this._contentWidget);
|
||||
}
|
||||
|
||||
dispose(helper: CodeLensHelper, viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor): void {
|
||||
while (this._decorationIds.length) {
|
||||
helper.removeDecoration(this._decorationIds.pop());
|
||||
}
|
||||
if (viewZoneChangeAccessor) {
|
||||
viewZoneChangeAccessor.removeZone(this._viewZoneId);
|
||||
}
|
||||
this._editor.removeContentWidget(this._contentWidget);
|
||||
|
||||
this._contentWidget.dispose();
|
||||
}
|
||||
|
||||
isValid(): boolean {
|
||||
return this._decorationIds.some((id, i) => {
|
||||
const range = this._editor.getModel().getDecorationRange(id);
|
||||
const symbol = this._data[i].symbol;
|
||||
return range && Range.isEmpty(symbol.range) === range.isEmpty();
|
||||
});
|
||||
}
|
||||
|
||||
updateCodeLensSymbols(data: ICodeLensData[], helper: CodeLensHelper): void {
|
||||
while (this._decorationIds.length) {
|
||||
helper.removeDecoration(this._decorationIds.pop());
|
||||
}
|
||||
this._data = data;
|
||||
this._decorationIds = new Array<string>(this._data.length);
|
||||
this._data.forEach((codeLensData, i) => {
|
||||
helper.addDecoration({
|
||||
range: codeLensData.symbol.range,
|
||||
options: ModelDecorationOptions.EMPTY
|
||||
}, id => this._decorationIds[i] = id);
|
||||
});
|
||||
}
|
||||
|
||||
computeIfNecessary(model: editorCommon.IModel): ICodeLensData[] {
|
||||
this._contentWidget.updateVisibility(); // trigger the fade in
|
||||
if (!this._contentWidget.isVisible()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read editor current state
|
||||
for (let i = 0; i < this._decorationIds.length; i++) {
|
||||
this._data[i].symbol.range = model.getDecorationRange(this._decorationIds[i]);
|
||||
}
|
||||
return this._data;
|
||||
}
|
||||
|
||||
updateCommands(symbols: ICodeLensSymbol[]): void {
|
||||
this._contentWidget.withCommands(symbols);
|
||||
}
|
||||
|
||||
getLineNumber(): number {
|
||||
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
|
||||
if (range) {
|
||||
return range.startLineNumber;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
update(viewZoneChangeAccessor: editorBrowser.IViewZoneChangeAccessor): void {
|
||||
if (this.isValid()) {
|
||||
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
|
||||
|
||||
this._viewZone.afterLineNumber = range.startLineNumber - 1;
|
||||
viewZoneChangeAccessor.layoutZone(this._viewZoneId);
|
||||
|
||||
this._contentWidget.setSymbolRange(range);
|
||||
this._editor.layoutContentWidget(this._contentWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let codeLensForeground = theme.getColor(editorCodeLensForeground);
|
||||
if (codeLensForeground) {
|
||||
collector.addRule(`.monaco-editor .codelens-decoration { color: ${codeLensForeground}; }`);
|
||||
}
|
||||
let activeLinkForeground = theme.getColor(editorActiveLinkForeground);
|
||||
if (activeLinkForeground) {
|
||||
collector.addRule(`.monaco-editor .codelens-decoration > a:hover { color: ${activeLinkForeground} !important; }`);
|
||||
}
|
||||
});
|
||||
242
src/vs/editor/contrib/colorPicker/browser/colorDetector.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RGBA } from 'vs/base/common/color';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ICommonCodeEditor, IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { ColorProviderRegistry, IColorRange } from 'vs/editor/common/modes';
|
||||
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
|
||||
import { getColors } from 'vs/editor/contrib/colorPicker/common/color';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
const MAX_DECORATORS = 500;
|
||||
|
||||
@editorContribution
|
||||
export class ColorDetector implements IEditorContribution {
|
||||
|
||||
private static ID: string = 'editor.contrib.colorDetector';
|
||||
|
||||
static RECOMPUTE_TIME = 1000; // ms
|
||||
|
||||
private _globalToDispose: IDisposable[] = [];
|
||||
private _localToDispose: IDisposable[] = [];
|
||||
private _computePromise: TPromise<void>;
|
||||
private _timeoutPromise: TPromise<void>;
|
||||
|
||||
private _decorationsIds: string[] = [];
|
||||
private _colorRanges = new Map<string, IColorRange>();
|
||||
|
||||
private _colorDecoratorIds: string[] = [];
|
||||
private _decorationsTypes: { [key: string]: boolean } = {};
|
||||
|
||||
private _isEnabled: boolean;
|
||||
|
||||
constructor(private _editor: ICodeEditor,
|
||||
@ICodeEditorService private _codeEditorService: ICodeEditorService,
|
||||
@IConfigurationService private _configurationService: IConfigurationService
|
||||
) {
|
||||
this._globalToDispose.push(_editor.onDidChangeModel((e) => {
|
||||
this._isEnabled = this.isEnabled();
|
||||
this.onModelChanged();
|
||||
}));
|
||||
this._globalToDispose.push(_editor.onDidChangeModelLanguage((e) => this.onModelChanged()));
|
||||
this._globalToDispose.push(ColorProviderRegistry.onDidChange((e) => this.onModelChanged()));
|
||||
this._globalToDispose.push(_editor.onDidChangeConfiguration((e) => {
|
||||
let prevIsEnabled = this._isEnabled;
|
||||
this._isEnabled = this.isEnabled();
|
||||
if (prevIsEnabled !== this._isEnabled) {
|
||||
if (this._isEnabled) {
|
||||
this.onModelChanged();
|
||||
} else {
|
||||
this.removeAllDecorations();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._timeoutPromise = null;
|
||||
this._computePromise = null;
|
||||
this._isEnabled = this.isEnabled();
|
||||
this.onModelChanged();
|
||||
}
|
||||
|
||||
isEnabled(): boolean {
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return false;
|
||||
}
|
||||
const languageId = model.getLanguageIdentifier();
|
||||
// handle deprecated settings. [languageId].colorDecorators.enable
|
||||
let deprecatedConfig = this._configurationService.getConfiguration(languageId.language);
|
||||
if (deprecatedConfig) {
|
||||
let colorDecorators = deprecatedConfig['colorDecorators']; // deprecatedConfig.valueOf('.colorDecorators.enable');
|
||||
if (colorDecorators && colorDecorators['enable'] !== undefined && !colorDecorators['enable']) {
|
||||
return colorDecorators['enable'];
|
||||
}
|
||||
}
|
||||
|
||||
return this._editor.getConfiguration().contribInfo.colorDecorators;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return ColorDetector.ID;
|
||||
}
|
||||
|
||||
static get(editor: ICommonCodeEditor): ColorDetector {
|
||||
return editor.getContribution<ColorDetector>(this.ID);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
this.removeAllDecorations();
|
||||
this._globalToDispose = dispose(this._globalToDispose);
|
||||
}
|
||||
|
||||
private onModelChanged(): void {
|
||||
this.stop();
|
||||
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
const model = this._editor.getModel();
|
||||
// if (!model) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!ColorProviderRegistry.has(model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._localToDispose.push(this._editor.onDidChangeModelContent((e) => {
|
||||
if (!this._timeoutPromise) {
|
||||
this._timeoutPromise = TPromise.timeout(ColorDetector.RECOMPUTE_TIME);
|
||||
this._timeoutPromise.then(() => {
|
||||
this._timeoutPromise = null;
|
||||
this.beginCompute();
|
||||
});
|
||||
}
|
||||
}));
|
||||
this.beginCompute();
|
||||
}
|
||||
|
||||
private beginCompute(): void {
|
||||
this._computePromise = getColors(this._editor.getModel()).then(colorInfos => {
|
||||
this.updateDecorations(colorInfos);
|
||||
this.updateColorDecorators(colorInfos);
|
||||
this._computePromise = null;
|
||||
});
|
||||
}
|
||||
|
||||
private stop(): void {
|
||||
if (this._timeoutPromise) {
|
||||
this._timeoutPromise.cancel();
|
||||
this._timeoutPromise = null;
|
||||
}
|
||||
if (this._computePromise) {
|
||||
this._computePromise.cancel();
|
||||
this._computePromise = null;
|
||||
}
|
||||
this._localToDispose = dispose(this._localToDispose);
|
||||
}
|
||||
|
||||
private updateDecorations(colorInfos: IColorRange[]): void {
|
||||
const decorations = colorInfos.map(c => ({
|
||||
range: {
|
||||
startLineNumber: c.range.startLineNumber,
|
||||
startColumn: c.range.startColumn,
|
||||
endLineNumber: c.range.endLineNumber,
|
||||
endColumn: c.range.endColumn
|
||||
},
|
||||
options: {}
|
||||
}));
|
||||
|
||||
const colorRanges = colorInfos.map(c => ({
|
||||
range: c.range,
|
||||
color: c.color,
|
||||
formatters: c.formatters
|
||||
}));
|
||||
|
||||
this._decorationsIds = this._editor.deltaDecorations(this._decorationsIds, decorations);
|
||||
|
||||
this._colorRanges = new Map<string, IColorRange>();
|
||||
this._decorationsIds.forEach((id, i) => this._colorRanges.set(id, colorRanges[i]));
|
||||
}
|
||||
|
||||
private updateColorDecorators(colorInfos: IColorRange[]): void {
|
||||
let decorations = [];
|
||||
let newDecorationsTypes: { [key: string]: boolean } = {};
|
||||
|
||||
for (let i = 0; i < colorInfos.length && decorations.length < MAX_DECORATORS; i++) {
|
||||
const { red, green, blue, alpha } = colorInfos[i].color;
|
||||
const rgba = new RGBA(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), alpha);
|
||||
let subKey = hash(rgba).toString(16);
|
||||
let color = `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;
|
||||
let key = 'colorBox-' + subKey;
|
||||
|
||||
if (!this._decorationsTypes[key] && !newDecorationsTypes[key]) {
|
||||
this._codeEditorService.registerDecorationType(key, {
|
||||
before: {
|
||||
contentText: ' ',
|
||||
border: 'solid 0.1em #000',
|
||||
margin: '0.1em 0.2em 0 0.2em',
|
||||
width: '0.8em',
|
||||
height: '0.8em',
|
||||
backgroundColor: color
|
||||
},
|
||||
dark: {
|
||||
before: {
|
||||
border: 'solid 0.1em #eee'
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newDecorationsTypes[key] = true;
|
||||
decorations.push({
|
||||
range: {
|
||||
startLineNumber: colorInfos[i].range.startLineNumber,
|
||||
startColumn: colorInfos[i].range.startColumn,
|
||||
endLineNumber: colorInfos[i].range.endLineNumber,
|
||||
endColumn: colorInfos[i].range.endColumn
|
||||
},
|
||||
options: this._codeEditorService.resolveDecorationOptions(key, true)
|
||||
});
|
||||
}
|
||||
|
||||
for (let subType in this._decorationsTypes) {
|
||||
if (!newDecorationsTypes[subType]) {
|
||||
this._codeEditorService.removeDecorationType(subType);
|
||||
}
|
||||
}
|
||||
|
||||
this._colorDecoratorIds = this._editor.deltaDecorations(this._colorDecoratorIds, decorations);
|
||||
}
|
||||
|
||||
private removeAllDecorations(): void {
|
||||
this._decorationsIds = this._editor.deltaDecorations(this._decorationsIds, []);
|
||||
this._colorDecoratorIds = this._editor.deltaDecorations(this._colorDecoratorIds, []);
|
||||
|
||||
for (let subType in this._decorationsTypes) {
|
||||
this._codeEditorService.removeDecorationType(subType);
|
||||
}
|
||||
}
|
||||
|
||||
getColorRange(position: Position): IColorRange | null {
|
||||
const decorations = this._editor.getModel()
|
||||
.getDecorationsInRange(Range.fromPositions(position, position))
|
||||
.filter(d => this._colorRanges.has(d.id));
|
||||
|
||||
if (decorations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._colorRanges.get(decorations[0].id);
|
||||
}
|
||||
}
|
||||
118
src/vs/editor/contrib/colorPicker/browser/colorPicker.css
Normal file
@@ -0,0 +1,118 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.colorpicker-widget {
|
||||
height: 190px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.monaco-shell .colorpicker-hover[tabindex="0"]:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
/* Header */
|
||||
|
||||
.colorpicker-header {
|
||||
display: flex;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
background: url('images/opacity-background.png');
|
||||
background-size: 9px 9px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.colorpicker-header .picked-color {
|
||||
width: 216px;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.colorpicker-header .picked-color.light {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.colorpicker-header .original-color {
|
||||
width: 74px;
|
||||
z-index: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
/* Body */
|
||||
|
||||
.colorpicker-body {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.colorpicker-body .saturation-wrap {
|
||||
overflow: hidden;
|
||||
height: 150px;
|
||||
position: relative;
|
||||
min-width: 220px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.colorpicker-body .saturation-box {
|
||||
height: 150px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.colorpicker-body .saturation-selection {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
margin: -5px 0 0 -5px;
|
||||
border: 1px solid rgb(255, 255, 255);
|
||||
border-radius: 100%;
|
||||
box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.8);
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.colorpicker-body .strip {
|
||||
width: 25px;
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
.colorpicker-body .hue-strip {
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
cursor: -webkit-grab;
|
||||
background: linear-gradient(to bottom, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
|
||||
}
|
||||
|
||||
.colorpicker-body .opacity-strip {
|
||||
position: relative;
|
||||
margin-left: 8px;
|
||||
cursor: -webkit-grab;
|
||||
background: url('images/opacity-background.png');
|
||||
background-size: 9px 9px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.colorpicker-body .strip.grabbing {
|
||||
cursor: -webkit-grabbing;
|
||||
}
|
||||
|
||||
.colorpicker-body .slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -2px;
|
||||
width: calc(100% + 4px);
|
||||
height: 4px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(255, 255, 255, 0.71);
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.colorpicker-body .strip .overlay {
|
||||
height: 150px;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { IColorFormatter } from 'vs/editor/common/modes';
|
||||
|
||||
function canFormat(formatter: IColorFormatter, color: Color): boolean {
|
||||
return color.isOpaque() || formatter.supportsTransparency;
|
||||
}
|
||||
|
||||
export class ColorPickerModel {
|
||||
|
||||
readonly originalColor: Color;
|
||||
private _color: Color;
|
||||
|
||||
get color(): Color {
|
||||
return this._color;
|
||||
}
|
||||
|
||||
set color(color: Color) {
|
||||
if (this._color.equals(color)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._color = color;
|
||||
this._checkFormat();
|
||||
this._onDidChangeColor.fire(color);
|
||||
}
|
||||
|
||||
get formatter(): IColorFormatter { return this.formatters[this.formatterIndex]; }
|
||||
|
||||
readonly formatters: IColorFormatter[];
|
||||
|
||||
private _onColorFlushed = new Emitter<Color>();
|
||||
readonly onColorFlushed: Event<Color> = this._onColorFlushed.event;
|
||||
|
||||
private _onDidChangeColor = new Emitter<Color>();
|
||||
readonly onDidChangeColor: Event<Color> = this._onDidChangeColor.event;
|
||||
|
||||
private _onDidChangeFormatter = new Emitter<IColorFormatter>();
|
||||
readonly onDidChangeFormatter: Event<IColorFormatter> = this._onDidChangeFormatter.event;
|
||||
|
||||
constructor(color: Color, availableFormatters: IColorFormatter[], private formatterIndex: number) {
|
||||
if (availableFormatters.length === 0) {
|
||||
throw new Error('Color picker needs formats');
|
||||
}
|
||||
|
||||
if (formatterIndex < 0 || formatterIndex >= availableFormatters.length) {
|
||||
throw new Error('Formatter index out of bounds');
|
||||
}
|
||||
|
||||
this.originalColor = color;
|
||||
this.formatters = availableFormatters;
|
||||
this._color = color;
|
||||
}
|
||||
|
||||
selectNextColorFormat(): void {
|
||||
const oldFomatterIndex = this.formatterIndex;
|
||||
this._checkFormat((this.formatterIndex + 1) % this.formatters.length);
|
||||
if (oldFomatterIndex !== this.formatterIndex) {
|
||||
this.flushColor();
|
||||
}
|
||||
}
|
||||
|
||||
flushColor(): void {
|
||||
this._onColorFlushed.fire(this._color);
|
||||
}
|
||||
|
||||
private _checkFormat(start = this.formatterIndex): void {
|
||||
let isNewFormat = this.formatterIndex !== start;
|
||||
this.formatterIndex = start;
|
||||
|
||||
while (!canFormat(this.formatter, this._color)) {
|
||||
this.formatterIndex = (this.formatterIndex + 1) % this.formatters.length;
|
||||
|
||||
if (this.formatterIndex === start) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (isNewFormat) {
|
||||
this._onDidChangeFormatter.fire(this.formatter);
|
||||
}
|
||||
}
|
||||
}
|
||||
361
src/vs/editor/contrib/colorPicker/browser/colorPickerWidget.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./colorPicker';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { onDidChangeZoomLevel } from 'vs/base/browser/browser';
|
||||
import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/browser/colorPickerModel';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
|
||||
import { Color, RGBA, HSVA } from 'vs/base/common/color';
|
||||
import { editorHoverBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class ColorPickerHeader extends Disposable {
|
||||
|
||||
private domNode: HTMLElement;
|
||||
private pickedColorNode: HTMLElement;
|
||||
private backgroundColor: Color;
|
||||
|
||||
constructor(container: HTMLElement, private model: ColorPickerModel) {
|
||||
super();
|
||||
|
||||
this.domNode = $('.colorpicker-header');
|
||||
dom.append(container, this.domNode);
|
||||
|
||||
this.pickedColorNode = dom.append(this.domNode, $('.picked-color'));
|
||||
|
||||
const colorBox = dom.append(this.domNode, $('.original-color'));
|
||||
colorBox.style.backgroundColor = Color.Format.CSS.format(this.model.originalColor);
|
||||
|
||||
this._register(registerThemingParticipant((theme, collector) => {
|
||||
this.backgroundColor = theme.getColor(editorHoverBackground) || Color.white;
|
||||
}));
|
||||
|
||||
this._register(dom.addDisposableListener(this.pickedColorNode, dom.EventType.CLICK, () => this.model.selectNextColorFormat()));
|
||||
this._register(dom.addDisposableListener(colorBox, dom.EventType.CLICK, () => {
|
||||
this.model.color = this.model.originalColor;
|
||||
this.model.flushColor();
|
||||
}));
|
||||
this._register(model.onDidChangeColor(this.onDidChangeColor, this));
|
||||
this._register(model.onDidChangeFormatter(this.onDidChangeFormatter, this));
|
||||
this.onDidChangeColor(this.model.color);
|
||||
}
|
||||
|
||||
private onDidChangeColor(color: Color): void {
|
||||
this.pickedColorNode.style.backgroundColor = Color.Format.CSS.format(color);
|
||||
dom.toggleClass(this.pickedColorNode, 'light', color.rgba.a < 0.5 ? this.backgroundColor.isLighter() : color.isLighter());
|
||||
this.onDidChangeFormatter();
|
||||
}
|
||||
|
||||
private onDidChangeFormatter(): void {
|
||||
this.pickedColorNode.textContent = this.model.formatter.format({
|
||||
red: this.model.color.rgba.r / 255,
|
||||
green: this.model.color.rgba.g / 255,
|
||||
blue: this.model.color.rgba.b / 255,
|
||||
alpha: this.model.color.rgba.a
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ColorPickerBody extends Disposable {
|
||||
|
||||
private domNode: HTMLElement;
|
||||
private saturationBox: SaturationBox;
|
||||
private hueStrip: Strip;
|
||||
private opacityStrip: Strip;
|
||||
|
||||
constructor(private container: HTMLElement, private model: ColorPickerModel, private pixelRatio: number) {
|
||||
super();
|
||||
|
||||
this.domNode = $('.colorpicker-body');
|
||||
dom.append(container, this.domNode);
|
||||
|
||||
this.saturationBox = new SaturationBox(this.domNode, this.model, this.pixelRatio);
|
||||
this._register(this.saturationBox);
|
||||
this._register(this.saturationBox.onDidChange(this.onDidSaturationValueChange, this));
|
||||
this._register(this.saturationBox.onColorFlushed(this.flushColor, this));
|
||||
|
||||
this.opacityStrip = new OpacityStrip(this.domNode, this.model);
|
||||
this._register(this.opacityStrip);
|
||||
this._register(this.opacityStrip.onDidChange(this.onDidOpacityChange, this));
|
||||
this._register(this.opacityStrip.onColorFlushed(this.flushColor, this));
|
||||
|
||||
this.hueStrip = new HueStrip(this.domNode, this.model);
|
||||
this._register(this.hueStrip);
|
||||
this._register(this.hueStrip.onDidChange(this.onDidHueChange, this));
|
||||
this._register(this.hueStrip.onColorFlushed(this.flushColor, this));
|
||||
}
|
||||
|
||||
private flushColor(): void {
|
||||
this.model.flushColor();
|
||||
}
|
||||
|
||||
private onDidSaturationValueChange({ s, v }: { s: number, v: number }): void {
|
||||
const hsva = this.model.color.hsva;
|
||||
this.model.color = new Color(new HSVA(hsva.h, s, v, hsva.a));
|
||||
}
|
||||
|
||||
private onDidOpacityChange(a: number): void {
|
||||
const hsva = this.model.color.hsva;
|
||||
this.model.color = new Color(new HSVA(hsva.h, hsva.s, hsva.v, a));
|
||||
}
|
||||
|
||||
private onDidHueChange(value: number): void {
|
||||
const hsva = this.model.color.hsva;
|
||||
const h = (1 - value) * 360;
|
||||
|
||||
this.model.color = new Color(new HSVA(h === 360 ? 0 : h, hsva.s, hsva.v, hsva.a));
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
this.saturationBox.layout();
|
||||
this.opacityStrip.layout();
|
||||
this.hueStrip.layout();
|
||||
}
|
||||
}
|
||||
|
||||
class SaturationBox extends Disposable {
|
||||
|
||||
private domNode: HTMLElement;
|
||||
private selection: HTMLElement;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private width: number;
|
||||
private height: number;
|
||||
|
||||
private monitor: GlobalMouseMoveMonitor<IStandardMouseMoveEventData>;
|
||||
private _onDidChange = new Emitter<{ s: number, v: number }>();
|
||||
readonly onDidChange: Event<{ s: number, v: number }> = this._onDidChange.event;
|
||||
|
||||
private _onColorFlushed = new Emitter<void>();
|
||||
readonly onColorFlushed: Event<void> = this._onColorFlushed.event;
|
||||
|
||||
constructor(container: HTMLElement, private model: ColorPickerModel, private pixelRatio: number) {
|
||||
super();
|
||||
|
||||
this.domNode = $('.saturation-wrap');
|
||||
dom.append(container, this.domNode);
|
||||
|
||||
// Create canvas, draw selected color
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.canvas.className = 'saturation-box';
|
||||
dom.append(this.domNode, this.canvas);
|
||||
|
||||
// Add selection circle
|
||||
this.selection = $('.saturation-selection');
|
||||
dom.append(this.domNode, this.selection);
|
||||
|
||||
this.layout();
|
||||
|
||||
this._register(dom.addDisposableListener(this.domNode, dom.EventType.MOUSE_DOWN, e => this.onMouseDown(e)));
|
||||
this._register(this.model.onDidChangeColor(this.onDidChangeColor, this));
|
||||
this.monitor = null;
|
||||
}
|
||||
|
||||
private onMouseDown(e: MouseEvent): void {
|
||||
this.monitor = this._register(new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>());
|
||||
const origin = dom.getDomNodePagePosition(this.domNode);
|
||||
|
||||
if (e.target !== this.selection) {
|
||||
this.onDidChangePosition(e.offsetX, e.offsetY);
|
||||
}
|
||||
|
||||
this.monitor.startMonitoring(standardMouseMoveMerger, event => this.onDidChangePosition(event.posx - origin.left, event.posy - origin.top), () => null);
|
||||
|
||||
const mouseUpListener = dom.addDisposableListener(document, dom.EventType.MOUSE_UP, () => {
|
||||
this._onColorFlushed.fire();
|
||||
mouseUpListener.dispose();
|
||||
this.monitor.stopMonitoring(true);
|
||||
this.monitor = null;
|
||||
}, true);
|
||||
}
|
||||
|
||||
private onDidChangePosition(left: number, top: number): void {
|
||||
const s = Math.max(0, Math.min(1, left / this.width));
|
||||
const v = Math.max(0, Math.min(1, 1 - (top / this.height)));
|
||||
|
||||
this.paintSelection(s, v);
|
||||
this._onDidChange.fire({ s, v });
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
this.width = this.domNode.offsetWidth;
|
||||
this.height = this.domNode.offsetHeight;
|
||||
this.canvas.width = this.width * this.pixelRatio;
|
||||
this.canvas.height = this.height * this.pixelRatio;
|
||||
this.paint();
|
||||
|
||||
const hsva = this.model.color.hsva;
|
||||
this.paintSelection(hsva.s, hsva.v);
|
||||
}
|
||||
|
||||
private paint(): void {
|
||||
const hsva = this.model.color.hsva;
|
||||
const saturatedColor = new Color(new HSVA(hsva.h, 1, 1, 1));
|
||||
const ctx = this.canvas.getContext('2d');
|
||||
|
||||
const whiteGradient = ctx.createLinearGradient(0, 0, this.canvas.width, 0);
|
||||
whiteGradient.addColorStop(0, 'rgba(255, 255, 255, 1)');
|
||||
whiteGradient.addColorStop(0.5, 'rgba(255, 255, 255, 0.5)');
|
||||
whiteGradient.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
||||
|
||||
const blackGradient = ctx.createLinearGradient(0, 0, 0, this.canvas.height);
|
||||
blackGradient.addColorStop(0, 'rgba(0, 0, 0, 0)');
|
||||
blackGradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
|
||||
|
||||
ctx.rect(0, 0, this.canvas.width, this.canvas.height);
|
||||
ctx.fillStyle = Color.Format.CSS.format(saturatedColor);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = whiteGradient;
|
||||
ctx.fill();
|
||||
ctx.fillStyle = blackGradient;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
private paintSelection(s: number, v: number): void {
|
||||
this.selection.style.left = `${s * this.width}px`;
|
||||
this.selection.style.top = `${this.height - v * this.height}px`;
|
||||
}
|
||||
|
||||
private onDidChangeColor(): void {
|
||||
if (this.monitor && this.monitor.isMonitoring()) {
|
||||
return;
|
||||
}
|
||||
this.paint();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class Strip extends Disposable {
|
||||
|
||||
protected domNode: HTMLElement;
|
||||
protected overlay: HTMLElement;
|
||||
protected slider: HTMLElement;
|
||||
private height: number;
|
||||
|
||||
private _onDidChange = new Emitter<number>();
|
||||
readonly onDidChange: Event<number> = this._onDidChange.event;
|
||||
|
||||
private _onColorFlushed = new Emitter<void>();
|
||||
readonly onColorFlushed: Event<void> = this._onColorFlushed.event;
|
||||
|
||||
constructor(container: HTMLElement, protected model: ColorPickerModel) {
|
||||
super();
|
||||
this.domNode = dom.append(container, $('.strip'));
|
||||
this.overlay = dom.append(this.domNode, $('.overlay'));
|
||||
this.slider = dom.append(this.domNode, $('.slider'));
|
||||
this.slider.style.top = `0px`;
|
||||
|
||||
this._register(dom.addDisposableListener(this.domNode, dom.EventType.MOUSE_DOWN, e => this.onMouseDown(e)));
|
||||
this.layout();
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
this.height = this.domNode.offsetHeight - this.slider.offsetHeight;
|
||||
|
||||
const value = this.getValue(this.model.color);
|
||||
this.updateSliderPosition(value);
|
||||
}
|
||||
|
||||
private onMouseDown(e: MouseEvent): void {
|
||||
const monitor = this._register(new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>());
|
||||
const origin = dom.getDomNodePagePosition(this.domNode);
|
||||
dom.addClass(this.domNode, 'grabbing');
|
||||
|
||||
if (e.target !== this.slider) {
|
||||
this.onDidChangeTop(e.offsetY);
|
||||
}
|
||||
|
||||
monitor.startMonitoring(standardMouseMoveMerger, event => this.onDidChangeTop(event.posy - origin.top), () => null);
|
||||
|
||||
const mouseUpListener = dom.addDisposableListener(document, dom.EventType.MOUSE_UP, () => {
|
||||
this._onColorFlushed.fire();
|
||||
mouseUpListener.dispose();
|
||||
monitor.stopMonitoring(true);
|
||||
dom.removeClass(this.domNode, 'grabbing');
|
||||
}, true);
|
||||
}
|
||||
|
||||
private onDidChangeTop(top: number): void {
|
||||
const value = Math.max(0, Math.min(1, 1 - (top / this.height)));
|
||||
|
||||
this.updateSliderPosition(value);
|
||||
this._onDidChange.fire(value);
|
||||
}
|
||||
|
||||
private updateSliderPosition(value: number): void {
|
||||
this.slider.style.top = `${(1 - value) * this.height}px`;
|
||||
}
|
||||
|
||||
protected abstract getValue(color: Color): number;
|
||||
}
|
||||
|
||||
class OpacityStrip extends Strip {
|
||||
|
||||
constructor(container: HTMLElement, model: ColorPickerModel) {
|
||||
super(container, model);
|
||||
dom.addClass(this.domNode, 'opacity-strip');
|
||||
|
||||
this._register(model.onDidChangeColor(this.onDidChangeColor, this));
|
||||
this.onDidChangeColor(this.model.color);
|
||||
}
|
||||
|
||||
private onDidChangeColor(color: Color): void {
|
||||
const { r, g, b } = color.rgba;
|
||||
const opaque = new Color(new RGBA(r, g, b, 1));
|
||||
const transparent = new Color(new RGBA(r, g, b, 0));
|
||||
|
||||
this.overlay.style.background = `linear-gradient(to bottom, ${opaque} 0%, ${transparent} 100%)`;
|
||||
}
|
||||
|
||||
protected getValue(color: Color): number {
|
||||
return color.hsva.a;
|
||||
}
|
||||
}
|
||||
|
||||
class HueStrip extends Strip {
|
||||
|
||||
constructor(container: HTMLElement, model: ColorPickerModel) {
|
||||
super(container, model);
|
||||
dom.addClass(this.domNode, 'hue-strip');
|
||||
}
|
||||
|
||||
protected getValue(color: Color): number {
|
||||
return 1 - (color.hsva.h / 360);
|
||||
}
|
||||
}
|
||||
|
||||
export class ColorPickerWidget extends Widget {
|
||||
|
||||
private static ID = 'editor.contrib.colorPickerWidget';
|
||||
|
||||
body: ColorPickerBody;
|
||||
|
||||
constructor(container: Node, private model: ColorPickerModel, private pixelRatio: number) {
|
||||
super();
|
||||
|
||||
this._register(onDidChangeZoomLevel(() => this.layout()));
|
||||
|
||||
const element = $('.colorpicker-widget');
|
||||
container.appendChild(element);
|
||||
|
||||
const header = new ColorPickerHeader(element, this.model);
|
||||
this.body = new ColorPickerBody(element, this.model, this.pixelRatio);
|
||||
|
||||
this._register(header);
|
||||
this._register(this.body);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return ColorPickerWidget.ID;
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
this.body.layout();
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 173 B |
18
src/vs/editor/contrib/colorPicker/common/color.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ColorProviderRegistry, IColorRange } from 'vs/editor/common/modes';
|
||||
import { asWinJsPromise } from 'vs/base/common/async';
|
||||
import { IReadOnlyModel } from 'vs/editor/common/editorCommon';
|
||||
import { flatten } from 'vs/base/common/arrays';
|
||||
|
||||
export function getColors(model: IReadOnlyModel): TPromise<IColorRange[]> {
|
||||
const providers = ColorProviderRegistry.ordered(model).reverse();
|
||||
const promises = providers.map(p => asWinJsPromise(token => p.provideColorRanges(model, token)));
|
||||
|
||||
return TPromise.join(promises)
|
||||
.then(ranges => flatten(ranges.filter(r => Array.isArray(r))));
|
||||
}
|
||||
144
src/vs/editor/contrib/colorPicker/common/colorFormatter.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IColorFormatter, IColor } from 'vs/editor/common/modes';
|
||||
import { Color, RGBA } from 'vs/base/common/color';
|
||||
|
||||
function roundFloat(number: number, decimalPoints: number): number {
|
||||
const decimal = Math.pow(10, decimalPoints);
|
||||
return Math.round(number * decimal) / decimal;
|
||||
}
|
||||
|
||||
interface Node {
|
||||
(color: Color): string;
|
||||
}
|
||||
|
||||
function createLiteralNode(value: string): Node {
|
||||
return () => value;
|
||||
}
|
||||
|
||||
function normalize(value: number, min: number, max: number): number {
|
||||
return value * (max - min) + min;
|
||||
}
|
||||
|
||||
function getPropertyValue(color: Color, variable: string): number | undefined {
|
||||
switch (variable) {
|
||||
case 'red':
|
||||
return color.rgba.r / 255;
|
||||
case 'green':
|
||||
return color.rgba.g / 255;
|
||||
case 'blue':
|
||||
return color.rgba.b / 255;
|
||||
case 'alpha':
|
||||
return color.rgba.a;
|
||||
case 'hue':
|
||||
return color.hsla.h / 360;
|
||||
case 'saturation':
|
||||
return color.hsla.s;
|
||||
case 'luminance':
|
||||
return color.hsla.l;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function createPropertyNode(variable: string, fractionDigits: number, type: string, min: number | undefined, max: number | undefined): Node {
|
||||
return color => {
|
||||
let value = getPropertyValue(color, variable);
|
||||
|
||||
if (value === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (type === 'd') {
|
||||
min = typeof min === 'number' ? min : 0;
|
||||
max = typeof max === 'number' ? max : 255;
|
||||
|
||||
return (normalize(value, min, max).toFixed(0)).toString();
|
||||
} else if (type === 'x' || type === 'X') {
|
||||
min = typeof min === 'number' ? min : 0;
|
||||
max = typeof max === 'number' ? max : 255;
|
||||
|
||||
let result = normalize(value, min, max).toString(16);
|
||||
|
||||
if (type === 'X') {
|
||||
result = result.toUpperCase();
|
||||
}
|
||||
|
||||
return result.length < 2 ? `0${result}` : result;
|
||||
}
|
||||
|
||||
min = typeof min === 'number' ? min : 0;
|
||||
max = typeof max === 'number' ? max : 1;
|
||||
return roundFloat(normalize(value, min, max), 2).toString();
|
||||
};
|
||||
}
|
||||
|
||||
export class ColorFormatter implements IColorFormatter {
|
||||
|
||||
readonly supportsTransparency: boolean = false;
|
||||
private tree: Node[] = [];
|
||||
|
||||
// Group 0: variable
|
||||
// Group 1: decimal digits
|
||||
// Group 2: floating/integer/hex
|
||||
// Group 3: range begin
|
||||
// Group 4: range end
|
||||
private static PATTERN = /{(\w+)(?::(\d*)(\w)+(?:\[(\d+)-(\d+)\])?)?}/g;
|
||||
|
||||
constructor(format: string) {
|
||||
let match = ColorFormatter.PATTERN.exec(format);
|
||||
let startIndex = 0;
|
||||
|
||||
// if no match -> erroor throw new Error(`${format} is not consistent with color format syntax.`);
|
||||
while (match !== null) {
|
||||
const index = match.index;
|
||||
|
||||
if (startIndex < index) {
|
||||
this.tree.push(createLiteralNode(format.substring(startIndex, index)));
|
||||
}
|
||||
|
||||
// add more parser catches
|
||||
const variable = match[1];
|
||||
if (!variable) {
|
||||
throw new Error(`${variable} is not defined.`);
|
||||
}
|
||||
|
||||
this.supportsTransparency = this.supportsTransparency || (variable === 'alpha');
|
||||
|
||||
const decimals = match[2] && parseInt(match[2]);
|
||||
const type = match[3];
|
||||
const startRange = match[4] && parseInt(match[4]);
|
||||
const endRange = match[5] && parseInt(match[5]);
|
||||
|
||||
this.tree.push(createPropertyNode(variable, decimals, type, startRange, endRange));
|
||||
|
||||
startIndex = index + match[0].length;
|
||||
match = ColorFormatter.PATTERN.exec(format);
|
||||
}
|
||||
|
||||
this.tree.push(createLiteralNode(format.substring(startIndex, format.length)));
|
||||
}
|
||||
|
||||
format(color: IColor): string {
|
||||
const richColor = new Color(new RGBA(Math.round(color.red * 255), Math.round(color.green * 255), Math.round(color.blue * 255), color.alpha));
|
||||
return this.tree.map(node => node(richColor)).join('');
|
||||
}
|
||||
}
|
||||
|
||||
export class CombinedColorFormatter implements IColorFormatter {
|
||||
|
||||
readonly supportsTransparency: boolean = true;
|
||||
|
||||
constructor(private opaqueFormatter: IColorFormatter, private transparentFormatter: IColorFormatter) {
|
||||
if (!transparentFormatter.supportsTransparency) {
|
||||
throw new Error('Invalid transparent formatter');
|
||||
}
|
||||
}
|
||||
|
||||
format(color: IColor): string {
|
||||
return color.alpha === 1 ? this.opaqueFormatter.format(color) : this.transparentFormatter.format(color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Color, RGBA, HSLA } from 'vs/base/common/color';
|
||||
import { IColor } from 'vs/editor/common/modes';
|
||||
import { ColorFormatter } from 'vs/editor/contrib/colorPicker/common/colorFormatter';
|
||||
|
||||
function convert2IColor(color: Color): IColor {
|
||||
return {
|
||||
red: color.rgba.r / 255,
|
||||
green: color.rgba.g / 255,
|
||||
blue: color.rgba.b / 255,
|
||||
alpha: color.rgba.a
|
||||
};
|
||||
}
|
||||
suite('ColorFormatter', () => {
|
||||
test('empty formatter', () => {
|
||||
const formatter = new ColorFormatter('');
|
||||
assert.equal(formatter.supportsTransparency, false);
|
||||
|
||||
assert.equal(formatter.format(convert2IColor(Color.white)), '');
|
||||
assert.equal(formatter.format(convert2IColor(Color.transparent)), '');
|
||||
});
|
||||
|
||||
test('no placeholder', () => {
|
||||
const formatter = new ColorFormatter('hello');
|
||||
assert.equal(formatter.supportsTransparency, false);
|
||||
|
||||
assert.equal(formatter.format(convert2IColor(Color.white)), 'hello');
|
||||
assert.equal(formatter.format(convert2IColor(Color.transparent)), 'hello');
|
||||
});
|
||||
|
||||
test('supportsTransparency', () => {
|
||||
const formatter = new ColorFormatter('hello');
|
||||
assert.equal(formatter.supportsTransparency, false);
|
||||
|
||||
const transparentFormatter = new ColorFormatter('{alpha}');
|
||||
assert.equal(transparentFormatter.supportsTransparency, true);
|
||||
});
|
||||
|
||||
test('default number format is float', () => {
|
||||
const formatter = new ColorFormatter('{red}');
|
||||
assert.equal(formatter.format(convert2IColor(Color.red)), '1');
|
||||
});
|
||||
|
||||
test('default decimal range is [0-255]', () => {
|
||||
const formatter = new ColorFormatter('{red:d}');
|
||||
assert.equal(formatter.format(convert2IColor(Color.red)), '255');
|
||||
});
|
||||
|
||||
test('default hex range is [0-FF]', () => {
|
||||
const formatter = new ColorFormatter('{red:X}');
|
||||
assert.equal(formatter.format(convert2IColor(Color.red)), 'FF');
|
||||
});
|
||||
|
||||
test('documentation', () => {
|
||||
const color = new Color(new RGBA(255, 127, 0));
|
||||
|
||||
const rgb = new ColorFormatter('rgb({red:d[0-255]}, {green:d[0-255]}, {blue:d[0-255]})');
|
||||
assert.equal(rgb.format(convert2IColor(color)), 'rgb(255, 127, 0)');
|
||||
|
||||
const rgba = new ColorFormatter('rgba({red:d[0-255]}, {green:d[0-255]}, {blue:d[0-255]}, {alpha})');
|
||||
assert.equal(rgba.format(convert2IColor(color)), 'rgba(255, 127, 0, 1)');
|
||||
|
||||
const hex = new ColorFormatter('#{red:X}{green:X}{blue:X}');
|
||||
assert.equal(hex.format(convert2IColor(color)), '#FF7F00');
|
||||
|
||||
const hsla = new ColorFormatter('hsla({hue:d[0-360]}, {saturation:d[0-100]}%, {luminance:d[0-100]}%, {alpha})');
|
||||
assert.equal(hsla.format(convert2IColor(color)), 'hsla(30, 100%, 50%, 1)');
|
||||
});
|
||||
|
||||
test('bug#32323', () => {
|
||||
const color = new Color(new HSLA(121, 0.45, 0.29, 0.61));
|
||||
const rgba = color.rgba;
|
||||
const color2 = new Color(new RGBA(rgba.r, rgba.g, rgba.b, rgba.a));
|
||||
const hsla = new ColorFormatter('hsla({hue:d[0-360]}, {saturation:d[0-100]}%, {luminance:d[0-100]}%, {alpha})');
|
||||
assert.equal(hsla.format(convert2IColor(color2)), 'hsla(121, 45%, 29%, 0.61)');
|
||||
});
|
||||
});
|
||||
174
src/vs/editor/contrib/comment/common/blockCommentCommand.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { ICommentsConfiguration, LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
|
||||
export class BlockCommentCommand implements editorCommon.ICommand {
|
||||
|
||||
private _selection: Selection;
|
||||
private _usedEndToken: string;
|
||||
|
||||
constructor(selection: Selection) {
|
||||
this._selection = selection;
|
||||
this._usedEndToken = null;
|
||||
}
|
||||
|
||||
public static _haystackHasNeedleAtOffset(haystack: string, needle: string, offset: number): boolean {
|
||||
if (offset < 0) {
|
||||
return false;
|
||||
}
|
||||
var needleLength = needle.length;
|
||||
var haystackLength = haystack.length;
|
||||
if (offset + needleLength > haystackLength) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < needleLength; i++) {
|
||||
if (haystack.charCodeAt(offset + i) !== needle.charCodeAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _createOperationsForBlockComment(selection: Range, config: ICommentsConfiguration, model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder): void {
|
||||
var startLineNumber = selection.startLineNumber;
|
||||
var startColumn = selection.startColumn;
|
||||
var endLineNumber = selection.endLineNumber;
|
||||
var endColumn = selection.endColumn;
|
||||
|
||||
var startToken = config.blockCommentStartToken;
|
||||
var endToken = config.blockCommentEndToken;
|
||||
|
||||
var startTokenIndex = model.getLineContent(startLineNumber).lastIndexOf(startToken, startColumn - 1 + startToken.length);
|
||||
var endTokenIndex = model.getLineContent(endLineNumber).indexOf(endToken, endColumn - 1 - endToken.length);
|
||||
|
||||
var ops: editorCommon.IIdentifiedSingleEditOperation[];
|
||||
|
||||
if (startTokenIndex !== -1 && endTokenIndex !== -1) {
|
||||
var endTokenBeforeCursorIndex = model.getLineContent(startLineNumber).lastIndexOf(endToken, startColumn - 1 + endToken.length);
|
||||
if (endTokenBeforeCursorIndex > startTokenIndex + startToken.length - 1) {
|
||||
ops = BlockCommentCommand._createAddBlockCommentOperations(selection, startToken, endToken);
|
||||
this._usedEndToken = ops.length === 1 ? endToken : null;
|
||||
} else {
|
||||
// We have to adjust to possible inner white space
|
||||
// For Space after startToken, add Space to startToken - range math will work out
|
||||
if (model.getLineContent(startLineNumber).charCodeAt(startTokenIndex + startToken.length) === CharCode.Space) {
|
||||
startToken += ' ';
|
||||
}
|
||||
// For Space before endToken, add Space before endToken and shift index one left
|
||||
if (model.getLineContent(endLineNumber).charCodeAt(endTokenIndex - 1) === CharCode.Space) {
|
||||
endToken = ' ' + endToken;
|
||||
endTokenIndex -= 1;
|
||||
}
|
||||
ops = BlockCommentCommand._createRemoveBlockCommentOperations(
|
||||
new Range(startLineNumber, startTokenIndex + 1 + startToken.length, endLineNumber, endTokenIndex + 1), startToken, endToken
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ops = BlockCommentCommand._createAddBlockCommentOperations(selection, startToken, endToken);
|
||||
this._usedEndToken = ops.length === 1 ? endToken : null;
|
||||
}
|
||||
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
builder.addTrackedEditOperation(ops[i].range, ops[i].text);
|
||||
}
|
||||
}
|
||||
|
||||
public static _createRemoveBlockCommentOperations(r: Range, startToken: string, endToken: string): editorCommon.IIdentifiedSingleEditOperation[] {
|
||||
var res: editorCommon.IIdentifiedSingleEditOperation[] = [];
|
||||
|
||||
if (!Range.isEmpty(r)) {
|
||||
// Remove block comment start
|
||||
res.push(EditOperation.delete(new Range(
|
||||
r.startLineNumber, r.startColumn - startToken.length,
|
||||
r.startLineNumber, r.startColumn
|
||||
)));
|
||||
|
||||
// Remove block comment end
|
||||
res.push(EditOperation.delete(new Range(
|
||||
r.endLineNumber, r.endColumn,
|
||||
r.endLineNumber, r.endColumn + endToken.length
|
||||
)));
|
||||
} else {
|
||||
// Remove both continuously
|
||||
res.push(EditOperation.delete(new Range(
|
||||
r.startLineNumber, r.startColumn - startToken.length,
|
||||
r.endLineNumber, r.endColumn + endToken.length
|
||||
)));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public static _createAddBlockCommentOperations(r: Range, startToken: string, endToken: string): editorCommon.IIdentifiedSingleEditOperation[] {
|
||||
var res: editorCommon.IIdentifiedSingleEditOperation[] = [];
|
||||
|
||||
if (!Range.isEmpty(r)) {
|
||||
// Insert block comment start
|
||||
res.push(EditOperation.insert(new Position(r.startLineNumber, r.startColumn), startToken + ' '));
|
||||
|
||||
// Insert block comment end
|
||||
res.push(EditOperation.insert(new Position(r.endLineNumber, r.endColumn), ' ' + endToken));
|
||||
} else {
|
||||
// Insert both continuously
|
||||
res.push(EditOperation.replace(new Range(
|
||||
r.startLineNumber, r.startColumn,
|
||||
r.endLineNumber, r.endColumn
|
||||
), startToken + ' ' + endToken));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
public getEditOperations(model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder): void {
|
||||
var startLineNumber = this._selection.startLineNumber;
|
||||
var startColumn = this._selection.startColumn;
|
||||
var endLineNumber = this._selection.endLineNumber;
|
||||
var endColumn = this._selection.endColumn;
|
||||
|
||||
model.tokenizeIfCheap(startLineNumber);
|
||||
let languageId = model.getLanguageIdAtPosition(startLineNumber, startColumn);
|
||||
let config = LanguageConfigurationRegistry.getComments(languageId);
|
||||
if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) {
|
||||
// Mode does not support block comments
|
||||
return;
|
||||
}
|
||||
|
||||
this._createOperationsForBlockComment(
|
||||
new Range(startLineNumber, startColumn, endLineNumber, endColumn), config, model, builder
|
||||
);
|
||||
}
|
||||
|
||||
public computeCursorState(model: editorCommon.ITokenizedModel, helper: editorCommon.ICursorStateComputerData): Selection {
|
||||
var inverseEditOperations = helper.getInverseEditOperations();
|
||||
if (inverseEditOperations.length === 2) {
|
||||
var startTokenEditOperation = inverseEditOperations[0];
|
||||
var endTokenEditOperation = inverseEditOperations[1];
|
||||
|
||||
return new Selection(
|
||||
startTokenEditOperation.range.endLineNumber,
|
||||
startTokenEditOperation.range.endColumn,
|
||||
endTokenEditOperation.range.startLineNumber,
|
||||
endTokenEditOperation.range.startColumn
|
||||
);
|
||||
} else {
|
||||
var srcRange = inverseEditOperations[0].range;
|
||||
var deltaColumn = this._usedEndToken ? -this._usedEndToken.length - 1 : 0; // minus 1 space before endToken
|
||||
return new Selection(
|
||||
srcRange.endLineNumber,
|
||||
srcRange.endColumn + deltaColumn,
|
||||
srcRange.endLineNumber,
|
||||
srcRange.endColumn + deltaColumn
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
122
src/vs/editor/contrib/comment/common/comment.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import { ICommand, ICommonCodeEditor } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { editorAction, IActionOptions, EditorAction, ServicesAccessor } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { BlockCommentCommand } from './blockCommentCommand';
|
||||
import { LineCommentCommand, Type } from './lineCommentCommand';
|
||||
|
||||
abstract class CommentLineAction extends EditorAction {
|
||||
|
||||
private _type: Type;
|
||||
|
||||
constructor(type: Type, opts: IActionOptions) {
|
||||
super(opts);
|
||||
this._type = type;
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
|
||||
let model = editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
var commands: ICommand[] = [];
|
||||
var selections = editor.getSelections();
|
||||
var opts = model.getOptions();
|
||||
|
||||
for (var i = 0; i < selections.length; i++) {
|
||||
commands.push(new LineCommentCommand(selections[i], opts.tabSize, this._type));
|
||||
}
|
||||
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, commands);
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class ToggleCommentLineAction extends CommentLineAction {
|
||||
constructor() {
|
||||
super(Type.Toggle, {
|
||||
id: 'editor.action.commentLine',
|
||||
label: nls.localize('comment.line', "Toggle Line Comment"),
|
||||
alias: 'Toggle Line Comment',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.US_SLASH
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class AddLineCommentAction extends CommentLineAction {
|
||||
constructor() {
|
||||
super(Type.ForceAdd, {
|
||||
id: 'editor.action.addCommentLine',
|
||||
label: nls.localize('comment.line.add', "Add Line Comment"),
|
||||
alias: 'Add Line Comment',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class RemoveLineCommentAction extends CommentLineAction {
|
||||
constructor() {
|
||||
super(Type.ForceRemove, {
|
||||
id: 'editor.action.removeCommentLine',
|
||||
label: nls.localize('comment.line.remove', "Remove Line Comment"),
|
||||
alias: 'Remove Line Comment',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class BlockCommentAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.blockComment',
|
||||
label: nls.localize('comment.block', "Toggle Block Comment"),
|
||||
alias: 'Toggle Block Comment',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A,
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
|
||||
var commands: ICommand[] = [];
|
||||
var selections = editor.getSelections();
|
||||
|
||||
for (var i = 0; i < selections.length; i++) {
|
||||
commands.push(new BlockCommentCommand(selections[i]));
|
||||
}
|
||||
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, commands);
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
447
src/vs/editor/contrib/comment/common/lineCommentCommand.ts
Normal file
@@ -0,0 +1,447 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { BlockCommentCommand } from './blockCommentCommand';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
|
||||
export interface IInsertionPoint {
|
||||
ignore: boolean;
|
||||
commentStrOffset: number;
|
||||
}
|
||||
|
||||
export interface ILinePreflightData {
|
||||
ignore: boolean;
|
||||
commentStr: string;
|
||||
commentStrOffset: number;
|
||||
commentStrLength: number;
|
||||
}
|
||||
|
||||
export interface IPreflightData {
|
||||
supported: boolean;
|
||||
shouldRemoveComments: boolean;
|
||||
lines: ILinePreflightData[];
|
||||
}
|
||||
|
||||
export interface ISimpleModel {
|
||||
getLineContent(lineNumber: number): string;
|
||||
}
|
||||
|
||||
export const enum Type {
|
||||
Toggle = 0,
|
||||
ForceAdd = 1,
|
||||
ForceRemove = 2
|
||||
}
|
||||
|
||||
export class LineCommentCommand implements editorCommon.ICommand {
|
||||
|
||||
private _selection: Selection;
|
||||
private _selectionId: string;
|
||||
private _deltaColumn: number;
|
||||
private _moveEndPositionDown: boolean;
|
||||
private _tabSize: number;
|
||||
private _type: Type;
|
||||
|
||||
constructor(selection: Selection, tabSize: number, type: Type) {
|
||||
this._selection = selection;
|
||||
this._tabSize = tabSize;
|
||||
this._type = type;
|
||||
this._deltaColumn = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do an initial pass over the lines and gather info about the line comment string.
|
||||
* Returns null if any of the lines doesn't support a line comment string.
|
||||
*/
|
||||
public static _gatherPreflightCommentStrings(model: editorCommon.ITokenizedModel, startLineNumber: number, endLineNumber: number): ILinePreflightData[] {
|
||||
|
||||
model.tokenizeIfCheap(startLineNumber);
|
||||
const languageId = model.getLanguageIdAtPosition(startLineNumber, 1);
|
||||
|
||||
const config = LanguageConfigurationRegistry.getComments(languageId);
|
||||
const commentStr = (config ? config.lineCommentToken : null);
|
||||
if (!commentStr) {
|
||||
// Mode does not support line comments
|
||||
return null;
|
||||
}
|
||||
|
||||
let lines: ILinePreflightData[] = [];
|
||||
for (let i = 0, lineCount = endLineNumber - startLineNumber + 1; i < lineCount; i++) {
|
||||
lines[i] = {
|
||||
ignore: false,
|
||||
commentStr: commentStr,
|
||||
commentStrOffset: 0,
|
||||
commentStrLength: commentStr.length
|
||||
};
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze lines and decide which lines are relevant and what the toggle should do.
|
||||
* Also, build up several offsets and lengths useful in the generation of editor operations.
|
||||
*/
|
||||
public static _analyzeLines(type: Type, model: ISimpleModel, lines: ILinePreflightData[], startLineNumber: number): IPreflightData {
|
||||
var lineData: ILinePreflightData,
|
||||
lineContentStartOffset: number,
|
||||
commentStrEndOffset: number,
|
||||
i: number,
|
||||
lineCount: number,
|
||||
lineNumber: number,
|
||||
shouldRemoveComments: boolean,
|
||||
lineContent: string,
|
||||
onlyWhitespaceLines = true;
|
||||
|
||||
if (type === Type.Toggle) {
|
||||
shouldRemoveComments = true;
|
||||
} else if (type === Type.ForceAdd) {
|
||||
shouldRemoveComments = false;
|
||||
} else {
|
||||
shouldRemoveComments = true;
|
||||
}
|
||||
|
||||
for (i = 0, lineCount = lines.length; i < lineCount; i++) {
|
||||
lineData = lines[i];
|
||||
lineNumber = startLineNumber + i;
|
||||
|
||||
lineContent = model.getLineContent(lineNumber);
|
||||
lineContentStartOffset = strings.firstNonWhitespaceIndex(lineContent);
|
||||
|
||||
if (lineContentStartOffset === -1) {
|
||||
// Empty or whitespace only line
|
||||
if (type === Type.Toggle) {
|
||||
lineData.ignore = true;
|
||||
} else if (type === Type.ForceAdd) {
|
||||
lineData.ignore = true;
|
||||
} else {
|
||||
lineData.ignore = true;
|
||||
}
|
||||
lineData.commentStrOffset = lineContent.length;
|
||||
continue;
|
||||
}
|
||||
|
||||
onlyWhitespaceLines = false;
|
||||
lineData.ignore = false;
|
||||
lineData.commentStrOffset = lineContentStartOffset;
|
||||
|
||||
if (shouldRemoveComments && !BlockCommentCommand._haystackHasNeedleAtOffset(lineContent, lineData.commentStr, lineContentStartOffset)) {
|
||||
if (type === Type.Toggle) {
|
||||
// Every line so far has been a line comment, but this one is not
|
||||
shouldRemoveComments = false;
|
||||
} else if (type === Type.ForceAdd) {
|
||||
// Will not happen
|
||||
} else {
|
||||
lineData.ignore = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldRemoveComments) {
|
||||
commentStrEndOffset = lineContentStartOffset + lineData.commentStrLength;
|
||||
if (commentStrEndOffset < lineContent.length && lineContent.charCodeAt(commentStrEndOffset) === CharCode.Space) {
|
||||
lineData.commentStrLength += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === Type.Toggle && onlyWhitespaceLines) {
|
||||
// For only whitespace lines, we insert comments
|
||||
shouldRemoveComments = false;
|
||||
|
||||
// Also, no longer ignore them
|
||||
for (i = 0, lineCount = lines.length; i < lineCount; i++) {
|
||||
lines[i].ignore = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
supported: true,
|
||||
shouldRemoveComments: shouldRemoveComments,
|
||||
lines: lines
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyze all lines and decide exactly what to do => not supported | insert line comments | remove line comments
|
||||
*/
|
||||
public static _gatherPreflightData(type: Type, model: editorCommon.ITokenizedModel, startLineNumber: number, endLineNumber: number): IPreflightData {
|
||||
var lines = LineCommentCommand._gatherPreflightCommentStrings(model, startLineNumber, endLineNumber);
|
||||
if (lines === null) {
|
||||
return {
|
||||
supported: false,
|
||||
shouldRemoveComments: false,
|
||||
lines: null
|
||||
};
|
||||
}
|
||||
|
||||
return LineCommentCommand._analyzeLines(type, model, lines, startLineNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a successful analysis, execute either insert line comments, either remove line comments
|
||||
*/
|
||||
private _executeLineComments(model: ISimpleModel, builder: editorCommon.IEditOperationBuilder, data: IPreflightData, s: Selection): void {
|
||||
|
||||
var ops: editorCommon.IIdentifiedSingleEditOperation[];
|
||||
|
||||
if (data.shouldRemoveComments) {
|
||||
ops = LineCommentCommand._createRemoveLineCommentsOperations(data.lines, s.startLineNumber);
|
||||
} else {
|
||||
LineCommentCommand._normalizeInsertionPoint(model, data.lines, s.startLineNumber, this._tabSize);
|
||||
ops = LineCommentCommand._createAddLineCommentsOperations(data.lines, s.startLineNumber);
|
||||
}
|
||||
|
||||
var cursorPosition = new Position(s.positionLineNumber, s.positionColumn);
|
||||
|
||||
for (var i = 0, len = ops.length; i < len; i++) {
|
||||
builder.addEditOperation(ops[i].range, ops[i].text);
|
||||
if (ops[i].range.isEmpty() && ops[i].range.getStartPosition().equals(cursorPosition)) {
|
||||
this._deltaColumn = ops[i].text.length;
|
||||
}
|
||||
}
|
||||
|
||||
this._selectionId = builder.trackSelection(s);
|
||||
}
|
||||
|
||||
private _attemptRemoveBlockComment(model: editorCommon.ITokenizedModel, s: Selection, startToken: string, endToken: string): editorCommon.IIdentifiedSingleEditOperation[] {
|
||||
let startLineNumber = s.startLineNumber;
|
||||
let endLineNumber = s.endLineNumber;
|
||||
|
||||
let startTokenAllowedBeforeColumn = endToken.length + Math.max(
|
||||
model.getLineFirstNonWhitespaceColumn(s.startLineNumber),
|
||||
s.startColumn
|
||||
);
|
||||
|
||||
let startTokenIndex = model.getLineContent(startLineNumber).lastIndexOf(startToken, startTokenAllowedBeforeColumn - 1);
|
||||
let endTokenIndex = model.getLineContent(endLineNumber).indexOf(endToken, s.endColumn - 1 - startToken.length);
|
||||
|
||||
if (startTokenIndex !== -1 && endTokenIndex === -1) {
|
||||
endTokenIndex = model.getLineContent(startLineNumber).indexOf(endToken, startTokenIndex + startToken.length);
|
||||
endLineNumber = startLineNumber;
|
||||
}
|
||||
|
||||
if (startTokenIndex === -1 && endTokenIndex !== -1) {
|
||||
startTokenIndex = model.getLineContent(endLineNumber).lastIndexOf(startToken, endTokenIndex);
|
||||
startLineNumber = endLineNumber;
|
||||
}
|
||||
|
||||
if (s.isEmpty() && (startTokenIndex === -1 || endTokenIndex === -1)) {
|
||||
startTokenIndex = model.getLineContent(startLineNumber).indexOf(startToken);
|
||||
if (startTokenIndex !== -1) {
|
||||
endTokenIndex = model.getLineContent(startLineNumber).indexOf(endToken, startTokenIndex + startToken.length);
|
||||
}
|
||||
}
|
||||
|
||||
// We have to adjust to possible inner white space.
|
||||
// For Space after startToken, add Space to startToken - range math will work out.
|
||||
if (startTokenIndex !== -1 && model.getLineContent(startLineNumber).charCodeAt(startTokenIndex + startToken.length) === CharCode.Space) {
|
||||
startToken += ' ';
|
||||
}
|
||||
|
||||
// For Space before endToken, add Space before endToken and shift index one left.
|
||||
if (endTokenIndex !== -1 && model.getLineContent(endLineNumber).charCodeAt(endTokenIndex - 1) === CharCode.Space) {
|
||||
endToken = ' ' + endToken;
|
||||
endTokenIndex -= 1;
|
||||
}
|
||||
|
||||
if (startTokenIndex !== -1 && endTokenIndex !== -1) {
|
||||
return BlockCommentCommand._createRemoveBlockCommentOperations(
|
||||
new Range(startLineNumber, startTokenIndex + startToken.length + 1, endLineNumber, endTokenIndex + 1), startToken, endToken
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an unsuccessful analysis, delegate to the block comment command
|
||||
*/
|
||||
private _executeBlockComment(model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder, s: Selection): void {
|
||||
model.tokenizeIfCheap(s.startLineNumber);
|
||||
let languageId = model.getLanguageIdAtPosition(s.startLineNumber, s.startColumn);
|
||||
let config = LanguageConfigurationRegistry.getComments(languageId);
|
||||
if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) {
|
||||
// Mode does not support block comments
|
||||
return;
|
||||
}
|
||||
|
||||
var startToken = config.blockCommentStartToken;
|
||||
var endToken = config.blockCommentEndToken;
|
||||
|
||||
var ops = this._attemptRemoveBlockComment(model, s, startToken, endToken);
|
||||
if (!ops) {
|
||||
if (s.isEmpty()) {
|
||||
var lineContent = model.getLineContent(s.startLineNumber);
|
||||
var firstNonWhitespaceIndex = strings.firstNonWhitespaceIndex(lineContent);
|
||||
if (firstNonWhitespaceIndex === -1) {
|
||||
// Line is empty or contains only whitespace
|
||||
firstNonWhitespaceIndex = lineContent.length;
|
||||
}
|
||||
ops = BlockCommentCommand._createAddBlockCommentOperations(
|
||||
new Range(s.startLineNumber, firstNonWhitespaceIndex + 1, s.startLineNumber, lineContent.length + 1), startToken, endToken
|
||||
);
|
||||
} else {
|
||||
ops = BlockCommentCommand._createAddBlockCommentOperations(
|
||||
new Range(s.startLineNumber, model.getLineFirstNonWhitespaceColumn(s.startLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), startToken, endToken
|
||||
);
|
||||
}
|
||||
|
||||
if (ops.length === 1) {
|
||||
// Leave cursor after token and Space
|
||||
this._deltaColumn = startToken.length + 1;
|
||||
}
|
||||
}
|
||||
this._selectionId = builder.trackSelection(s);
|
||||
for (var i = 0; i < ops.length; i++) {
|
||||
builder.addEditOperation(ops[i].range, ops[i].text);
|
||||
}
|
||||
}
|
||||
|
||||
public getEditOperations(model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder): void {
|
||||
|
||||
var s = this._selection;
|
||||
this._moveEndPositionDown = false;
|
||||
|
||||
if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {
|
||||
this._moveEndPositionDown = true;
|
||||
s = s.setEndPosition(s.endLineNumber - 1, model.getLineMaxColumn(s.endLineNumber - 1));
|
||||
}
|
||||
|
||||
var data = LineCommentCommand._gatherPreflightData(this._type, model, s.startLineNumber, s.endLineNumber);
|
||||
if (data.supported) {
|
||||
return this._executeLineComments(model, builder, data, s);
|
||||
}
|
||||
|
||||
return this._executeBlockComment(model, builder, s);
|
||||
}
|
||||
|
||||
public computeCursorState(model: editorCommon.ITokenizedModel, helper: editorCommon.ICursorStateComputerData): Selection {
|
||||
var result = helper.getTrackedSelection(this._selectionId);
|
||||
|
||||
if (this._moveEndPositionDown) {
|
||||
result = result.setEndPosition(result.endLineNumber + 1, 1);
|
||||
}
|
||||
|
||||
return new Selection(
|
||||
result.startLineNumber,
|
||||
result.startColumn + this._deltaColumn,
|
||||
result.endLineNumber,
|
||||
result.endColumn + this._deltaColumn
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate edit operations in the remove line comment case
|
||||
*/
|
||||
public static _createRemoveLineCommentsOperations(lines: ILinePreflightData[], startLineNumber: number): editorCommon.IIdentifiedSingleEditOperation[] {
|
||||
var i: number,
|
||||
len: number,
|
||||
lineData: ILinePreflightData,
|
||||
res: editorCommon.IIdentifiedSingleEditOperation[] = [];
|
||||
|
||||
for (i = 0, len = lines.length; i < len; i++) {
|
||||
lineData = lines[i];
|
||||
|
||||
if (lineData.ignore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
res.push(EditOperation.delete(new Range(
|
||||
startLineNumber + i, lineData.commentStrOffset + 1,
|
||||
startLineNumber + i, lineData.commentStrOffset + lineData.commentStrLength + 1
|
||||
)));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate edit operations in the add line comment case
|
||||
*/
|
||||
public static _createAddLineCommentsOperations(lines: ILinePreflightData[], startLineNumber: number): editorCommon.IIdentifiedSingleEditOperation[] {
|
||||
var i: number,
|
||||
len: number,
|
||||
lineData: ILinePreflightData,
|
||||
res: editorCommon.IIdentifiedSingleEditOperation[] = [];
|
||||
|
||||
for (i = 0, len = lines.length; i < len; i++) {
|
||||
lineData = lines[i];
|
||||
|
||||
if (lineData.ignore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
res.push(EditOperation.insert(new Position(startLineNumber + i, lineData.commentStrOffset + 1), lineData.commentStr + ' '));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
// TODO@Alex -> duplicated in characterHardWrappingLineMapper
|
||||
private static nextVisibleColumn(currentVisibleColumn: number, tabSize: number, isTab: boolean, columnSize: number): number {
|
||||
if (isTab) {
|
||||
return currentVisibleColumn + (tabSize - (currentVisibleColumn % tabSize));
|
||||
}
|
||||
return currentVisibleColumn + columnSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust insertion points to have them vertically aligned in the add line comment case
|
||||
*/
|
||||
public static _normalizeInsertionPoint(model: ISimpleModel, lines: IInsertionPoint[], startLineNumber: number, tabSize: number): void {
|
||||
var minVisibleColumn = Number.MAX_VALUE,
|
||||
i: number,
|
||||
len: number,
|
||||
lineContent: string,
|
||||
j: number,
|
||||
lenJ: number,
|
||||
currentVisibleColumn: number;
|
||||
|
||||
for (i = 0, len = lines.length; i < len; i++) {
|
||||
if (lines[i].ignore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lineContent = model.getLineContent(startLineNumber + i);
|
||||
|
||||
currentVisibleColumn = 0;
|
||||
for (j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) {
|
||||
currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, tabSize, lineContent.charCodeAt(j) === CharCode.Tab, 1);
|
||||
}
|
||||
|
||||
if (currentVisibleColumn < minVisibleColumn) {
|
||||
minVisibleColumn = currentVisibleColumn;
|
||||
}
|
||||
}
|
||||
|
||||
minVisibleColumn = Math.floor(minVisibleColumn / tabSize) * tabSize;
|
||||
|
||||
for (i = 0, len = lines.length; i < len; i++) {
|
||||
if (lines[i].ignore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
lineContent = model.getLineContent(startLineNumber + i);
|
||||
|
||||
currentVisibleColumn = 0;
|
||||
for (j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) {
|
||||
currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, tabSize, lineContent.charCodeAt(j) === CharCode.Tab, 1);
|
||||
}
|
||||
|
||||
if (currentVisibleColumn > minVisibleColumn) {
|
||||
lines[i].commentStrOffset = j - 1;
|
||||
} else {
|
||||
lines[i].commentStrOffset = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { BlockCommentCommand } from 'vs/editor/contrib/comment/common/blockCommentCommand';
|
||||
import { testCommand } from 'vs/editor/test/common/commands/commandTestUtils';
|
||||
import { CommentMode } from 'vs/editor/test/common/commentMode';
|
||||
|
||||
function testBlockCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void {
|
||||
let mode = new CommentMode({ lineComment: '!@#', blockComment: ['<0', '0>'] });
|
||||
testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new BlockCommentCommand(sel), expectedLines, expectedSelection);
|
||||
mode.dispose();
|
||||
}
|
||||
|
||||
suite('Editor Contrib - Block Comment Command', () => {
|
||||
|
||||
test('empty selection wraps itself', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 3, 1, 3),
|
||||
[
|
||||
'fi<0 0>rst',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 6, 1, 6)
|
||||
);
|
||||
});
|
||||
|
||||
test('invisible selection ignored', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 1, 1, 1),
|
||||
[
|
||||
'<0 first',
|
||||
' 0>\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 4, 2, 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('bug9511', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 6, 1, 1),
|
||||
[
|
||||
'<0 first 0>',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 4, 1, 9)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0first0>',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 8, 1, 3),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 1, 6)
|
||||
);
|
||||
});
|
||||
|
||||
test('one line selection', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 6, 1, 3),
|
||||
[
|
||||
'fi<0 rst 0>',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 6, 1, 9)
|
||||
);
|
||||
});
|
||||
|
||||
test('one line selection toggle', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 6, 1, 3),
|
||||
[
|
||||
'fi<0 rst 0>',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 6, 1, 9)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'fi<0rst0>',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 8, 1, 5),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 3, 1, 6)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0 first 0>',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 10, 1, 1),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 1, 6)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0 first0>',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 9, 1, 1),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 1, 6)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0first 0>',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 9, 1, 1),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 1, 6)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'fi<0rst0>',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 8, 1, 5),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 3, 1, 6)
|
||||
);
|
||||
});
|
||||
|
||||
test('multi line selection', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 4, 1, 1),
|
||||
[
|
||||
'<0 first',
|
||||
'\tse 0>cond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 4, 2, 4)
|
||||
);
|
||||
});
|
||||
|
||||
test('multi line selection toggle', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 4, 1, 1),
|
||||
[
|
||||
'<0 first',
|
||||
'\tse 0>cond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 4, 2, 4)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0first',
|
||||
'\tse0>cond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 4, 1, 3),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 2, 4)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0 first',
|
||||
'\tse0>cond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 4, 1, 3),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 2, 4)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0first',
|
||||
'\tse 0>cond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 4, 1, 3),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 2, 4)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0 first',
|
||||
'\tse 0>cond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 4, 1, 3),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 2, 4)
|
||||
);
|
||||
});
|
||||
|
||||
test('fuzzy removes', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'asd <0 qwe',
|
||||
'asd 0> qwe'
|
||||
],
|
||||
new Selection(2, 5, 1, 7),
|
||||
[
|
||||
'asd qwe',
|
||||
'asd qwe'
|
||||
],
|
||||
new Selection(1, 5, 2, 4)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'asd <0 qwe',
|
||||
'asd 0> qwe'
|
||||
],
|
||||
new Selection(2, 5, 1, 6),
|
||||
[
|
||||
'asd qwe',
|
||||
'asd qwe'
|
||||
],
|
||||
new Selection(1, 5, 2, 4)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'asd <0 qwe',
|
||||
'asd 0> qwe'
|
||||
],
|
||||
new Selection(2, 5, 1, 5),
|
||||
[
|
||||
'asd qwe',
|
||||
'asd qwe'
|
||||
],
|
||||
new Selection(1, 5, 2, 4)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'asd <0 qwe',
|
||||
'asd 0> qwe'
|
||||
],
|
||||
new Selection(2, 5, 1, 11),
|
||||
[
|
||||
'asd qwe',
|
||||
'asd qwe'
|
||||
],
|
||||
new Selection(1, 5, 2, 4)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'asd <0 qwe',
|
||||
'asd 0> qwe'
|
||||
],
|
||||
new Selection(2, 1, 1, 11),
|
||||
[
|
||||
'asd qwe',
|
||||
'asd qwe'
|
||||
],
|
||||
new Selection(1, 5, 2, 4)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'asd <0 qwe',
|
||||
'asd 0> qwe'
|
||||
],
|
||||
new Selection(2, 7, 1, 11),
|
||||
[
|
||||
'asd qwe',
|
||||
'asd qwe'
|
||||
],
|
||||
new Selection(1, 5, 2, 4)
|
||||
);
|
||||
});
|
||||
|
||||
test('bug #30358', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0 start 0> middle end',
|
||||
],
|
||||
new Selection(1, 20, 1, 23),
|
||||
[
|
||||
'<0 start 0> middle <0 end 0>'
|
||||
],
|
||||
new Selection(1, 23, 1, 26)
|
||||
);
|
||||
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0 start 0> middle <0 end 0>'
|
||||
],
|
||||
new Selection(1, 13, 1, 19),
|
||||
[
|
||||
'<0 start 0> <0 middle 0> <0 end 0>'
|
||||
],
|
||||
new Selection(1, 16, 1, 22)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,940 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ILinePreflightData, IPreflightData, ISimpleModel, LineCommentCommand, Type } from 'vs/editor/contrib/comment/common/lineCommentCommand';
|
||||
import { testCommand } from 'vs/editor/test/common/commands/commandTestUtils';
|
||||
import { CommentMode } from 'vs/editor/test/common/commentMode';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { NULL_STATE } from 'vs/editor/common/modes/nullMode';
|
||||
import { TokenizationResult2 } from 'vs/editor/common/core/token';
|
||||
import { MockMode } from 'vs/editor/test/common/mocks/mockMode';
|
||||
import { CommentRule } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
|
||||
suite('Editor Contrib - Line Comment Command', () => {
|
||||
|
||||
function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void {
|
||||
let mode = new CommentMode({ lineComment: '!@#', blockComment: ['<!@#', '#@!>'] });
|
||||
testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle), expectedLines, expectedSelection);
|
||||
mode.dispose();
|
||||
}
|
||||
|
||||
function testAddLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void {
|
||||
let mode = new CommentMode({ lineComment: '!@#', blockComment: ['<!@#', '#@!>'] });
|
||||
testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.ForceAdd), expectedLines, expectedSelection);
|
||||
mode.dispose();
|
||||
}
|
||||
|
||||
test('comment single line', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'some text',
|
||||
'\tsome more text'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
'!@# some text',
|
||||
'\tsome more text'
|
||||
],
|
||||
new Selection(1, 9, 1, 9)
|
||||
);
|
||||
});
|
||||
|
||||
function createSimpleModel(lines: string[]): ISimpleModel {
|
||||
return {
|
||||
getLineContent: (lineNumber: number) => {
|
||||
return lines[lineNumber - 1];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createBasicLinePreflightData(commentTokens: string[]): ILinePreflightData[] {
|
||||
return commentTokens.map((commentString) => {
|
||||
var r: ILinePreflightData = {
|
||||
ignore: false,
|
||||
commentStr: commentString,
|
||||
commentStrOffset: 0,
|
||||
commentStrLength: commentString.length
|
||||
};
|
||||
return r;
|
||||
});
|
||||
}
|
||||
|
||||
test('_analyzeLines', function () {
|
||||
var r: IPreflightData;
|
||||
|
||||
r = LineCommentCommand._analyzeLines(Type.Toggle, createSimpleModel([
|
||||
'\t\t',
|
||||
' ',
|
||||
' c',
|
||||
'\t\td'
|
||||
]), createBasicLinePreflightData(['//', 'rem', '!@#', '!@#']), 1);
|
||||
|
||||
assert.equal(r.shouldRemoveComments, false);
|
||||
|
||||
// Does not change `commentStr`
|
||||
assert.equal(r.lines[0].commentStr, '//');
|
||||
assert.equal(r.lines[1].commentStr, 'rem');
|
||||
assert.equal(r.lines[2].commentStr, '!@#');
|
||||
assert.equal(r.lines[3].commentStr, '!@#');
|
||||
|
||||
// Fills in `isWhitespace`
|
||||
assert.equal(r.lines[0].ignore, true);
|
||||
assert.equal(r.lines[1].ignore, true);
|
||||
assert.equal(r.lines[2].ignore, false);
|
||||
assert.equal(r.lines[3].ignore, false);
|
||||
|
||||
// Fills in `commentStrOffset`
|
||||
assert.equal(r.lines[0].commentStrOffset, 2);
|
||||
assert.equal(r.lines[1].commentStrOffset, 4);
|
||||
assert.equal(r.lines[2].commentStrOffset, 4);
|
||||
assert.equal(r.lines[3].commentStrOffset, 2);
|
||||
|
||||
|
||||
r = LineCommentCommand._analyzeLines(Type.Toggle, createSimpleModel([
|
||||
'\t\t',
|
||||
' rem ',
|
||||
' !@# c',
|
||||
'\t\t!@#d'
|
||||
]), createBasicLinePreflightData(['//', 'rem', '!@#', '!@#']), 1);
|
||||
|
||||
assert.equal(r.shouldRemoveComments, true);
|
||||
|
||||
// Does not change `commentStr`
|
||||
assert.equal(r.lines[0].commentStr, '//');
|
||||
assert.equal(r.lines[1].commentStr, 'rem');
|
||||
assert.equal(r.lines[2].commentStr, '!@#');
|
||||
assert.equal(r.lines[3].commentStr, '!@#');
|
||||
|
||||
// Fills in `isWhitespace`
|
||||
assert.equal(r.lines[0].ignore, true);
|
||||
assert.equal(r.lines[1].ignore, false);
|
||||
assert.equal(r.lines[2].ignore, false);
|
||||
assert.equal(r.lines[3].ignore, false);
|
||||
|
||||
// Fills in `commentStrOffset`
|
||||
assert.equal(r.lines[0].commentStrOffset, 2);
|
||||
assert.equal(r.lines[1].commentStrOffset, 4);
|
||||
assert.equal(r.lines[2].commentStrOffset, 4);
|
||||
assert.equal(r.lines[3].commentStrOffset, 2);
|
||||
|
||||
// Fills in `commentStrLength`
|
||||
assert.equal(r.lines[0].commentStrLength, 2);
|
||||
assert.equal(r.lines[1].commentStrLength, 4);
|
||||
assert.equal(r.lines[2].commentStrLength, 4);
|
||||
assert.equal(r.lines[3].commentStrLength, 3);
|
||||
});
|
||||
|
||||
test('_normalizeInsertionPoint', function () {
|
||||
|
||||
var runTest = (mixedArr: any[], tabSize: number, expected: number[], testName: string) => {
|
||||
var model = createSimpleModel(mixedArr.filter((item, idx) => idx % 2 === 0));
|
||||
var offsets = mixedArr.filter((item, idx) => idx % 2 === 1).map(offset => {
|
||||
return {
|
||||
commentStrOffset: offset,
|
||||
ignore: false
|
||||
};
|
||||
});
|
||||
LineCommentCommand._normalizeInsertionPoint(model, offsets, 1, tabSize);
|
||||
var actual = offsets.map(item => item.commentStrOffset);
|
||||
assert.deepEqual(actual, expected, testName);
|
||||
};
|
||||
|
||||
// Bug 16696:[comment] comments not aligned in this case
|
||||
runTest([
|
||||
' XX', 2,
|
||||
' YY', 4
|
||||
], 4, [0, 0], 'Bug 16696');
|
||||
|
||||
runTest([
|
||||
'\t\t\tXX', 3,
|
||||
' \tYY', 5,
|
||||
' ZZ', 8,
|
||||
'\t\tTT', 2
|
||||
], 4, [2, 5, 8, 2], 'Test1');
|
||||
|
||||
runTest([
|
||||
'\t\t\t XX', 6,
|
||||
' \t\t\t\tYY', 8,
|
||||
' ZZ', 8,
|
||||
'\t\t TT', 6
|
||||
], 4, [2, 5, 8, 2], 'Test2');
|
||||
|
||||
runTest([
|
||||
'\t\t', 2,
|
||||
'\t\t\t', 3,
|
||||
'\t\t\t\t', 4,
|
||||
'\t\t\t', 3
|
||||
], 4, [2, 2, 2, 2], 'Test3');
|
||||
|
||||
runTest([
|
||||
'\t\t', 2,
|
||||
'\t\t\t', 3,
|
||||
'\t\t\t\t', 4,
|
||||
'\t\t\t', 3,
|
||||
' ', 4
|
||||
], 2, [2, 2, 2, 2, 4], 'Test4');
|
||||
|
||||
runTest([
|
||||
'\t\t', 2,
|
||||
'\t\t\t', 3,
|
||||
'\t\t\t\t', 4,
|
||||
'\t\t\t', 3,
|
||||
' ', 4
|
||||
], 4, [1, 1, 1, 1, 4], 'Test5');
|
||||
|
||||
runTest([
|
||||
' \t', 2,
|
||||
' \t', 3,
|
||||
' \t', 4,
|
||||
' ', 4,
|
||||
'\t', 1
|
||||
], 4, [2, 3, 4, 4, 1], 'Test6');
|
||||
|
||||
runTest([
|
||||
' \t\t', 3,
|
||||
' \t\t', 4,
|
||||
' \t\t', 5,
|
||||
' \t', 5,
|
||||
'\t', 1
|
||||
], 4, [2, 3, 4, 4, 1], 'Test7');
|
||||
|
||||
runTest([
|
||||
'\t', 1,
|
||||
' ', 4
|
||||
], 4, [1, 4], 'Test8:4');
|
||||
runTest([
|
||||
'\t', 1,
|
||||
' ', 3
|
||||
], 4, [0, 0], 'Test8:3');
|
||||
runTest([
|
||||
'\t', 1,
|
||||
' ', 2
|
||||
], 4, [0, 0], 'Test8:2');
|
||||
runTest([
|
||||
'\t', 1,
|
||||
' ', 1
|
||||
], 4, [0, 0], 'Test8:1');
|
||||
runTest([
|
||||
'\t', 1,
|
||||
'', 0
|
||||
], 4, [0, 0], 'Test8:0');
|
||||
});
|
||||
|
||||
test('detects indentation', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\tsome text',
|
||||
'\tsome more text'
|
||||
],
|
||||
new Selection(2, 2, 1, 1),
|
||||
[
|
||||
'\t!@# some text',
|
||||
'\t!@# some more text'
|
||||
],
|
||||
new Selection(1, 1, 2, 2)
|
||||
);
|
||||
});
|
||||
|
||||
test('detects mixed indentation', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\tsome text',
|
||||
' some more text'
|
||||
],
|
||||
new Selection(2, 2, 1, 1),
|
||||
[
|
||||
'\t!@# some text',
|
||||
' !@# some more text'
|
||||
],
|
||||
new Selection(1, 1, 2, 2)
|
||||
);
|
||||
});
|
||||
|
||||
test('ignores whitespace lines', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\tsome text',
|
||||
'\t ',
|
||||
'',
|
||||
'\tsome more text'
|
||||
],
|
||||
new Selection(4, 2, 1, 1),
|
||||
[
|
||||
'\t!@# some text',
|
||||
'\t ',
|
||||
'',
|
||||
'\t!@# some more text'
|
||||
],
|
||||
new Selection(1, 1, 4, 2)
|
||||
);
|
||||
});
|
||||
|
||||
test('removes its own', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t!@# some text',
|
||||
'\t ',
|
||||
'\t\t!@# some more text'
|
||||
],
|
||||
new Selection(3, 2, 1, 1),
|
||||
[
|
||||
'\tsome text',
|
||||
'\t ',
|
||||
'\t\tsome more text'
|
||||
],
|
||||
new Selection(1, 1, 3, 2)
|
||||
);
|
||||
});
|
||||
|
||||
test('works in only whitespace', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t ',
|
||||
'\t',
|
||||
'\t\tsome more text'
|
||||
],
|
||||
new Selection(3, 1, 1, 1),
|
||||
[
|
||||
'\t!@# ',
|
||||
'\t!@# ',
|
||||
'\t\tsome more text'
|
||||
],
|
||||
new Selection(1, 1, 3, 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('bug 9697 - whitespace before comment token', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t !@#first',
|
||||
'\tsecond line'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
'\t first',
|
||||
'\tsecond line'
|
||||
],
|
||||
new Selection(1, 1, 1, 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('bug 10162 - line comment before caret', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first!@#',
|
||||
'\tsecond line'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
'!@# first!@#',
|
||||
'\tsecond line'
|
||||
],
|
||||
new Selection(1, 9, 1, 9)
|
||||
);
|
||||
});
|
||||
|
||||
test('comment single line - leading whitespace', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first!@#',
|
||||
'\tsecond line'
|
||||
],
|
||||
new Selection(2, 3, 2, 1),
|
||||
[
|
||||
'first!@#',
|
||||
'\t!@# second line'
|
||||
],
|
||||
new Selection(2, 1, 2, 7)
|
||||
);
|
||||
});
|
||||
|
||||
test('ignores invisible selection', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 1, 1, 1),
|
||||
[
|
||||
'!@# first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 9, 2, 5)
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple lines', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 4, 1, 1),
|
||||
[
|
||||
'!@# first',
|
||||
'!@# \tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 9, 2, 12)
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple modes on multiple lines', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(4, 4, 3, 1),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'!@# third line',
|
||||
'!@# fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(3, 9, 4, 12)
|
||||
);
|
||||
});
|
||||
|
||||
test('toggle single line', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
'!@# first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 9, 1, 9)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'!@# first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 4, 1, 4),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 1, 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('toggle multiple lines', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 4, 1, 1),
|
||||
[
|
||||
'!@# first',
|
||||
'!@# \tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 9, 2, 12)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'!@# first',
|
||||
'!@# \tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 7, 1, 4),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 2, 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('issue #2837 "Add Line Comment" fault when blank lines involved', function () {
|
||||
testAddLineCommentCommand(
|
||||
[
|
||||
' if displayName == "":',
|
||||
' displayName = groupName',
|
||||
' description = getAttr(attributes, "description")',
|
||||
' mailAddress = getAttr(attributes, "mail")',
|
||||
'',
|
||||
' print "||Group name|%s|" % displayName',
|
||||
' print "||Description|%s|" % description',
|
||||
' print "||Email address|[mailto:%s]|" % mailAddress`',
|
||||
],
|
||||
new Selection(1, 1, 8, 56),
|
||||
[
|
||||
' !@# if displayName == "":',
|
||||
' !@# displayName = groupName',
|
||||
' !@# description = getAttr(attributes, "description")',
|
||||
' !@# mailAddress = getAttr(attributes, "mail")',
|
||||
'',
|
||||
' !@# print "||Group name|%s|" % displayName',
|
||||
' !@# print "||Description|%s|" % description',
|
||||
' !@# print "||Email address|[mailto:%s]|" % mailAddress`',
|
||||
],
|
||||
new Selection(1, 1, 8, 60)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Editor Contrib - Line Comment As Block Comment', () => {
|
||||
|
||||
function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void {
|
||||
let mode = new CommentMode({ lineComment: '', blockComment: ['(', ')'] });
|
||||
testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle), expectedLines, expectedSelection);
|
||||
mode.dispose();
|
||||
}
|
||||
|
||||
test('fall back to block comment command', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
'( first )',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 3, 1, 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('fall back to block comment command - toggle', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'(first)',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 7, 1, 2),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 1, 6)
|
||||
);
|
||||
});
|
||||
|
||||
test('bug 9513 - expand single line to uncomment auto block', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
'( first )',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 3, 1, 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('bug 9691 - always expand selection to line boundaries', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(3, 2, 1, 3),
|
||||
[
|
||||
'( first',
|
||||
'\tsecond line',
|
||||
'third line )',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 5, 3, 2)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'(first',
|
||||
'\tsecond line',
|
||||
'third line)',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(3, 11, 1, 2),
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 1, 3, 11)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Editor Contrib - Line Comment As Block Comment 2', () => {
|
||||
function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void {
|
||||
let mode = new CommentMode({ lineComment: null, blockComment: ['<!@#', '#@!>'] });
|
||||
testCommand(lines, mode.getLanguageIdentifier(), selection, (sel) => new LineCommentCommand(sel, 4, Type.Toggle), expectedLines, expectedSelection);
|
||||
mode.dispose();
|
||||
}
|
||||
|
||||
test('no selection => uses indentation', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
'\t\t<!@# first\t #@!>',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(1, 1, 1, 1)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t\t<!@#first\t #@!>',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(1, 1, 1, 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('can remove', function () {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(5, 1, 5, 1),
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\tfifth\t\t'
|
||||
],
|
||||
new Selection(5, 1, 5, 1)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(5, 3, 5, 3),
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\tfifth\t\t'
|
||||
],
|
||||
new Selection(5, 3, 5, 3)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(5, 4, 5, 4),
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\tfifth\t\t'
|
||||
],
|
||||
new Selection(5, 3, 5, 3)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(5, 16, 5, 3),
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\tfifth\t\t'
|
||||
],
|
||||
new Selection(5, 3, 5, 8)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(5, 12, 5, 7),
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\tfifth\t\t'
|
||||
],
|
||||
new Selection(5, 3, 5, 8)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\t<!@#fifth#@!>\t\t'
|
||||
],
|
||||
new Selection(5, 18, 5, 18),
|
||||
[
|
||||
'\t\tfirst\t ',
|
||||
'\t\tsecond line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'\t\tfifth\t\t'
|
||||
],
|
||||
new Selection(5, 10, 5, 10)
|
||||
);
|
||||
});
|
||||
|
||||
test('issue #993: Remove comment does not work consistently in HTML', () => {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
' asd qwe',
|
||||
' asd qwe',
|
||||
''
|
||||
],
|
||||
new Selection(1, 1, 3, 1),
|
||||
[
|
||||
' <!@# asd qwe',
|
||||
' asd qwe #@!>',
|
||||
''
|
||||
],
|
||||
new Selection(1, 1, 3, 1)
|
||||
);
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
' <!@#asd qwe',
|
||||
' asd qwe#@!>',
|
||||
''
|
||||
],
|
||||
new Selection(1, 1, 3, 1),
|
||||
[
|
||||
' asd qwe',
|
||||
' asd qwe',
|
||||
''
|
||||
],
|
||||
new Selection(1, 1, 3, 1)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Editor Contrib - Line Comment in mixed modes', () => {
|
||||
|
||||
const OUTER_LANGUAGE_ID = new modes.LanguageIdentifier('outerMode', 3);
|
||||
const INNER_LANGUAGE_ID = new modes.LanguageIdentifier('innerMode', 4);
|
||||
|
||||
class OuterMode extends MockMode {
|
||||
constructor(commentsConfig: CommentRule) {
|
||||
super(OUTER_LANGUAGE_ID);
|
||||
this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {
|
||||
comments: commentsConfig
|
||||
}));
|
||||
|
||||
this._register(modes.TokenizationRegistry.register(this.getLanguageIdentifier().language, {
|
||||
getInitialState: (): modes.IState => NULL_STATE,
|
||||
tokenize: undefined,
|
||||
tokenize2: (line: string, state: modes.IState): TokenizationResult2 => {
|
||||
let languageId = (/^ /.test(line) ? INNER_LANGUAGE_ID : OUTER_LANGUAGE_ID);
|
||||
|
||||
let tokens = new Uint32Array(1 << 1);
|
||||
tokens[(0 << 1)] = 0;
|
||||
tokens[(0 << 1) + 1] = (
|
||||
(modes.ColorId.DefaultForeground << modes.MetadataConsts.FOREGROUND_OFFSET)
|
||||
| (languageId.id << modes.MetadataConsts.LANGUAGEID_OFFSET)
|
||||
);
|
||||
return new TokenizationResult2(tokens, state);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
class InnerMode extends MockMode {
|
||||
constructor(commentsConfig: CommentRule) {
|
||||
super(INNER_LANGUAGE_ID);
|
||||
this._register(LanguageConfigurationRegistry.register(this.getLanguageIdentifier(), {
|
||||
comments: commentsConfig
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function testLineCommentCommand(lines: string[], selection: Selection, expectedLines: string[], expectedSelection: Selection): void {
|
||||
let outerMode = new OuterMode({ lineComment: '//', blockComment: ['/*', '*/'] });
|
||||
let innerMode = new InnerMode({ lineComment: null, blockComment: ['{/*', '*/}'] });
|
||||
testCommand(
|
||||
lines,
|
||||
outerMode.getLanguageIdentifier(),
|
||||
selection,
|
||||
(sel) => new LineCommentCommand(sel, 4, Type.Toggle),
|
||||
expectedLines,
|
||||
expectedSelection
|
||||
);
|
||||
innerMode.dispose();
|
||||
outerMode.dispose();
|
||||
}
|
||||
|
||||
test('issue #24047 (part 1): Commenting code in JSX files', () => {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'import React from \'react\';',
|
||||
'const Loader = () => (',
|
||||
' <div>',
|
||||
' Loading...',
|
||||
' </div>',
|
||||
');',
|
||||
'export default Loader;'
|
||||
],
|
||||
new Selection(1, 1, 7, 22),
|
||||
[
|
||||
'// import React from \'react\';',
|
||||
'// const Loader = () => (',
|
||||
'// <div>',
|
||||
'// Loading...',
|
||||
'// </div>',
|
||||
'// );',
|
||||
'// export default Loader;'
|
||||
],
|
||||
new Selection(1, 4, 7, 25),
|
||||
);
|
||||
});
|
||||
|
||||
test('issue #24047 (part 2): Commenting code in JSX files', () => {
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'import React from \'react\';',
|
||||
'const Loader = () => (',
|
||||
' <div>',
|
||||
' Loading...',
|
||||
' </div>',
|
||||
');',
|
||||
'export default Loader;'
|
||||
],
|
||||
new Selection(3, 4, 3, 4),
|
||||
[
|
||||
'import React from \'react\';',
|
||||
'const Loader = () => (',
|
||||
' {/* <div> */}',
|
||||
' Loading...',
|
||||
' </div>',
|
||||
');',
|
||||
'export default Loader;'
|
||||
],
|
||||
new Selection(3, 8, 3, 8),
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
240
src/vs/editor/contrib/contextmenu/browser/contextmenu.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { ResolvedKeybinding, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { ActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IMenuService, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { ICommonCodeEditor, IEditorContribution, IScrollEvent, ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { editorAction, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
|
||||
export interface IPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
@editorContribution
|
||||
export class ContextMenuController implements IEditorContribution {
|
||||
|
||||
private static ID = 'editor.contrib.contextmenu';
|
||||
|
||||
public static get(editor: ICommonCodeEditor): ContextMenuController {
|
||||
return editor.getContribution<ContextMenuController>(ContextMenuController.ID);
|
||||
}
|
||||
|
||||
private _toDispose: IDisposable[] = [];
|
||||
private _contextMenuIsBeingShownCount: number = 0;
|
||||
private _editor: ICodeEditor;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextMenuService private _contextMenuService: IContextMenuService,
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
@IContextKeyService private _contextKeyService: IContextKeyService,
|
||||
@IKeybindingService private _keybindingService: IKeybindingService,
|
||||
@IMenuService private _menuService: IMenuService
|
||||
) {
|
||||
this._editor = editor;
|
||||
|
||||
this._toDispose.push(this._editor.onContextMenu((e: IEditorMouseEvent) => this._onContextMenu(e)));
|
||||
this._toDispose.push(this._editor.onDidScrollChange((e: IScrollEvent) => {
|
||||
if (this._contextMenuIsBeingShownCount > 0) {
|
||||
this._contextViewService.hideContextView();
|
||||
}
|
||||
}));
|
||||
this._toDispose.push(this._editor.onKeyDown((e: IKeyboardEvent) => {
|
||||
if (e.keyCode === KeyCode.ContextMenu) {
|
||||
// Chrome is funny like that
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.showContextMenu();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private _onContextMenu(e: IEditorMouseEvent): void {
|
||||
if (!this._editor.getConfiguration().contribInfo.contextmenu) {
|
||||
this._editor.focus();
|
||||
// Ensure the cursor is at the position of the mouse click
|
||||
if (e.target.position && !this._editor.getSelection().containsPosition(e.target.position)) {
|
||||
this._editor.setPosition(e.target.position);
|
||||
}
|
||||
return; // Context menu is turned off through configuration
|
||||
}
|
||||
|
||||
if (e.target.type === MouseTargetType.OVERLAY_WIDGET) {
|
||||
return; // allow native menu on widgets to support right click on input field for example in find
|
||||
}
|
||||
|
||||
e.event.preventDefault();
|
||||
|
||||
if (e.target.type !== MouseTargetType.CONTENT_TEXT && e.target.type !== MouseTargetType.CONTENT_EMPTY && e.target.type !== MouseTargetType.TEXTAREA) {
|
||||
return; // only support mouse click into text or native context menu key for now
|
||||
}
|
||||
|
||||
// Ensure the editor gets focus if it hasn't, so the right events are being sent to other contributions
|
||||
this._editor.focus();
|
||||
|
||||
// Ensure the cursor is at the position of the mouse click
|
||||
if (e.target.position && !this._editor.getSelection().containsPosition(e.target.position)) {
|
||||
this._editor.setPosition(e.target.position);
|
||||
}
|
||||
|
||||
// Unless the user triggerd the context menu through Shift+F10, use the mouse position as menu position
|
||||
var forcedPosition: IPosition;
|
||||
if (e.target.type !== MouseTargetType.TEXTAREA) {
|
||||
forcedPosition = { x: e.event.posx, y: e.event.posy + 1 };
|
||||
}
|
||||
|
||||
// Show the context menu
|
||||
this.showContextMenu(forcedPosition);
|
||||
}
|
||||
|
||||
public showContextMenu(forcedPosition?: IPosition): void {
|
||||
if (!this._editor.getConfiguration().contribInfo.contextmenu) {
|
||||
return; // Context menu is turned off through configuration
|
||||
}
|
||||
|
||||
if (!this._contextMenuService) {
|
||||
this._editor.focus();
|
||||
return; // We need the context menu service to function
|
||||
}
|
||||
|
||||
// Find actions available for menu
|
||||
var menuActions = this._getMenuActions();
|
||||
|
||||
// Show menu if we have actions to show
|
||||
if (menuActions.length > 0) {
|
||||
this._doShowContextMenu(menuActions, forcedPosition);
|
||||
}
|
||||
}
|
||||
|
||||
private _getMenuActions(): IAction[] {
|
||||
const result: IAction[] = [];
|
||||
|
||||
let contextMenu = this._menuService.createMenu(MenuId.EditorContext, this._contextKeyService);
|
||||
const groups = contextMenu.getActions({ arg: this._editor.getModel().uri });
|
||||
contextMenu.dispose();
|
||||
|
||||
for (let group of groups) {
|
||||
const [, actions] = group;
|
||||
result.push(...actions);
|
||||
result.push(new Separator());
|
||||
}
|
||||
result.pop(); // remove last separator
|
||||
return result;
|
||||
}
|
||||
|
||||
private _doShowContextMenu(actions: IAction[], forcedPosition: IPosition = null): void {
|
||||
|
||||
// Disable hover
|
||||
var oldHoverSetting = this._editor.getConfiguration().contribInfo.hover;
|
||||
this._editor.updateOptions({
|
||||
hover: false
|
||||
});
|
||||
|
||||
var menuPosition = forcedPosition;
|
||||
if (!menuPosition) {
|
||||
// Ensure selection is visible
|
||||
this._editor.revealPosition(this._editor.getPosition(), ScrollType.Immediate);
|
||||
|
||||
this._editor.render();
|
||||
var cursorCoords = this._editor.getScrolledVisiblePosition(this._editor.getPosition());
|
||||
|
||||
// Translate to absolute editor position
|
||||
var editorCoords = dom.getDomNodePagePosition(this._editor.getDomNode());
|
||||
var posx = editorCoords.left + cursorCoords.left;
|
||||
var posy = editorCoords.top + cursorCoords.top + cursorCoords.height;
|
||||
|
||||
menuPosition = { x: posx, y: posy };
|
||||
}
|
||||
|
||||
// Show menu
|
||||
this._contextMenuIsBeingShownCount++;
|
||||
this._contextMenuService.showContextMenu({
|
||||
getAnchor: () => menuPosition,
|
||||
|
||||
getActions: () => {
|
||||
return TPromise.as(actions);
|
||||
},
|
||||
|
||||
getActionItem: (action) => {
|
||||
var keybinding = this._keybindingFor(action);
|
||||
if (keybinding) {
|
||||
return new ActionItem(action, action, { label: true, keybinding: keybinding.getLabel(), isMenu: true });
|
||||
}
|
||||
|
||||
var customActionItem = <any>action;
|
||||
if (typeof customActionItem.getActionItem === 'function') {
|
||||
return customActionItem.getActionItem();
|
||||
}
|
||||
|
||||
return new ActionItem(action, action, { icon: true, label: true, isMenu: true });
|
||||
},
|
||||
|
||||
getKeyBinding: (action): ResolvedKeybinding => {
|
||||
return this._keybindingFor(action);
|
||||
},
|
||||
|
||||
onHide: (wasCancelled: boolean) => {
|
||||
this._contextMenuIsBeingShownCount--;
|
||||
this._editor.focus();
|
||||
this._editor.updateOptions({
|
||||
hover: oldHoverSetting
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _keybindingFor(action: IAction): ResolvedKeybinding {
|
||||
return this._keybindingService.lookupKeybinding(action.id);
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return ContextMenuController.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._contextMenuIsBeingShownCount > 0) {
|
||||
this._contextViewService.hideContextView();
|
||||
}
|
||||
|
||||
this._toDispose = dispose(this._toDispose);
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class ShowContextMenu extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.showContextMenu',
|
||||
label: nls.localize('action.showContextMenu.label', "Show Editor Context Menu"),
|
||||
alias: 'Show Editor Context Menu',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.Shift | KeyCode.F10
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
|
||||
let contribution = ContextMenuController.get(editor);
|
||||
contribution.showContextMenu();
|
||||
}
|
||||
}
|
||||
128
src/vs/editor/contrib/cursorUndo/browser/cursorUndo.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { editorCommand, ServicesAccessor, EditorCommand } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ICommonCodeEditor, IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
|
||||
class CursorState {
|
||||
readonly selections: Selection[];
|
||||
|
||||
constructor(selections: Selection[]) {
|
||||
this.selections = selections;
|
||||
}
|
||||
|
||||
public equals(other: CursorState): boolean {
|
||||
const thisLen = this.selections.length;
|
||||
const otherLen = other.selections.length;
|
||||
if (thisLen !== otherLen) {
|
||||
return false;
|
||||
}
|
||||
for (let i = 0; i < thisLen; i++) {
|
||||
if (!this.selections[i].equalsSelection(other.selections[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@editorContribution
|
||||
export class CursorUndoController extends Disposable implements IEditorContribution {
|
||||
|
||||
private static ID = 'editor.contrib.cursorUndoController';
|
||||
|
||||
public static get(editor: ICommonCodeEditor): CursorUndoController {
|
||||
return editor.getContribution<CursorUndoController>(CursorUndoController.ID);
|
||||
}
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
private _isCursorUndo: boolean;
|
||||
|
||||
private _undoStack: CursorState[];
|
||||
private _prevState: CursorState;
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
super();
|
||||
this._editor = editor;
|
||||
this._isCursorUndo = false;
|
||||
|
||||
this._undoStack = [];
|
||||
this._prevState = this._readState();
|
||||
|
||||
this._register(editor.onDidChangeModel((e) => {
|
||||
this._undoStack = [];
|
||||
this._prevState = null;
|
||||
}));
|
||||
this._register(editor.onDidChangeModelContent((e) => {
|
||||
this._undoStack = [];
|
||||
this._prevState = null;
|
||||
}));
|
||||
this._register(editor.onDidChangeCursorSelection((e) => {
|
||||
|
||||
if (!this._isCursorUndo && this._prevState) {
|
||||
this._undoStack.push(this._prevState);
|
||||
if (this._undoStack.length > 50) {
|
||||
// keep the cursor undo stack bounded
|
||||
this._undoStack = this._undoStack.splice(0, this._undoStack.length - 50);
|
||||
}
|
||||
}
|
||||
|
||||
this._prevState = this._readState();
|
||||
}));
|
||||
}
|
||||
|
||||
private _readState(): CursorState {
|
||||
if (!this._editor.getModel()) {
|
||||
// no model => no state
|
||||
return null;
|
||||
}
|
||||
|
||||
return new CursorState(this._editor.getSelections());
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return CursorUndoController.ID;
|
||||
}
|
||||
|
||||
public cursorUndo(): void {
|
||||
const currState = new CursorState(this._editor.getSelections());
|
||||
|
||||
while (this._undoStack.length > 0) {
|
||||
const prevState = this._undoStack.pop();
|
||||
|
||||
if (!prevState.equals(currState)) {
|
||||
this._isCursorUndo = true;
|
||||
this._editor.setSelections(prevState.selections);
|
||||
this._isCursorUndo = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@editorCommand
|
||||
export class CursorUndo extends EditorCommand {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'cursorUndo',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_U
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public runEditorCommand(accessor: ServicesAccessor, editor: ICommonCodeEditor, args: any): void {
|
||||
CursorUndoController.get(editor).cursorUndo();
|
||||
}
|
||||
}
|
||||
28
src/vs/editor/contrib/dnd/browser/dnd.css
Normal file
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor.vs .dnd-target {
|
||||
border-right: 2px dotted black;
|
||||
color: white; /* opposite of black */
|
||||
}
|
||||
.monaco-editor.vs-dark .dnd-target {
|
||||
border-right: 2px dotted #AEAFAD;
|
||||
color: #51504f; /* opposite of #AEAFAD */
|
||||
}
|
||||
.monaco-editor.hc-black .dnd-target {
|
||||
border-right: 2px dotted #fff;
|
||||
color: #000; /* opposite of #fff */
|
||||
}
|
||||
|
||||
.monaco-editor.mouse-default .view-lines,
|
||||
.monaco-editor.vs-dark.mac.mouse-default .view-lines,
|
||||
.monaco-editor.hc-black.mac.mouse-default .view-lines {
|
||||
cursor: default;
|
||||
}
|
||||
.monaco-editor.mouse-copy .view-lines,
|
||||
.monaco-editor.vs-dark.mac.mouse-copy .view-lines,
|
||||
.monaco-editor.hc-black.mac.mouse-copy .view-lines {
|
||||
cursor: copy;
|
||||
}
|
||||
210
src/vs/editor/contrib/dnd/browser/dnd.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./dnd';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ICodeEditor, IEditorMouseEvent, IMouseTarget, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { DragAndDropCommand } from '../common/dragAndDropCommand';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations';
|
||||
|
||||
@editorContribution
|
||||
export class DragAndDropController implements editorCommon.IEditorContribution {
|
||||
|
||||
private static ID = 'editor.contrib.dragAndDrop';
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _toUnhook: IDisposable[];
|
||||
private _dragSelection: Selection;
|
||||
private _dndDecorationIds: string[];
|
||||
private _mouseDown: boolean;
|
||||
private _modiferPressed: boolean;
|
||||
static TRIGGER_MODIFIER = isMacintosh ? 'altKey' : 'ctrlKey';
|
||||
static TRIGGER_KEY_VALUE = isMacintosh ? KeyCode.Alt : KeyCode.Ctrl;
|
||||
|
||||
static get(editor: editorCommon.ICommonCodeEditor): DragAndDropController {
|
||||
return editor.getContribution<DragAndDropController>(DragAndDropController.ID);
|
||||
}
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
this._editor = editor;
|
||||
this._toUnhook = [];
|
||||
this._toUnhook.push(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e)));
|
||||
this._toUnhook.push(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(e)));
|
||||
this._toUnhook.push(this._editor.onMouseDrag((e: IEditorMouseEvent) => this._onEditorMouseDrag(e)));
|
||||
this._toUnhook.push(this._editor.onMouseDrop((e: IEditorMouseEvent) => this._onEditorMouseDrop(e)));
|
||||
this._toUnhook.push(this._editor.onKeyDown((e: IKeyboardEvent) => this.onEditorKeyDown(e)));
|
||||
this._toUnhook.push(this._editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(e)));
|
||||
this._dndDecorationIds = [];
|
||||
this._mouseDown = false;
|
||||
this._modiferPressed = false;
|
||||
this._dragSelection = null;
|
||||
}
|
||||
|
||||
private onEditorKeyDown(e: IKeyboardEvent): void {
|
||||
if (!this._editor.getConfiguration().dragAndDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e[DragAndDropController.TRIGGER_MODIFIER]) {
|
||||
this._modiferPressed = true;
|
||||
}
|
||||
|
||||
if (this._mouseDown && e[DragAndDropController.TRIGGER_MODIFIER]) {
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'copy'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onEditorKeyUp(e: IKeyboardEvent): void {
|
||||
if (!this._editor.getConfiguration().dragAndDrop) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e[DragAndDropController.TRIGGER_MODIFIER]) {
|
||||
this._modiferPressed = false;
|
||||
}
|
||||
|
||||
if (this._mouseDown && e.keyCode === DragAndDropController.TRIGGER_KEY_VALUE) {
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'default'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {
|
||||
this._mouseDown = true;
|
||||
}
|
||||
|
||||
private _onEditorMouseUp(mouseEvent: IEditorMouseEvent): void {
|
||||
this._mouseDown = false;
|
||||
// Whenever users release the mouse, the drag and drop operation should finish and the cursor should revert to text.
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'text'
|
||||
});
|
||||
}
|
||||
|
||||
private _onEditorMouseDrag(mouseEvent: IEditorMouseEvent): void {
|
||||
let target = mouseEvent.target;
|
||||
|
||||
if (this._dragSelection === null) {
|
||||
let possibleSelections = this._editor.getSelections().filter(selection => selection.containsPosition(target.position));
|
||||
if (possibleSelections.length === 1) {
|
||||
this._dragSelection = possibleSelections[0];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (mouseEvent.event[DragAndDropController.TRIGGER_MODIFIER]) {
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'copy'
|
||||
});
|
||||
} else {
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'default'
|
||||
});
|
||||
}
|
||||
|
||||
if (this._dragSelection.containsPosition(target.position)) {
|
||||
this._removeDecoration();
|
||||
} else {
|
||||
this.showAt(target.position);
|
||||
}
|
||||
}
|
||||
|
||||
private _onEditorMouseDrop(mouseEvent: IEditorMouseEvent): void {
|
||||
if (mouseEvent.target && (this._hitContent(mouseEvent.target) || this._hitMargin(mouseEvent.target)) && mouseEvent.target.position) {
|
||||
let newCursorPosition = new Position(mouseEvent.target.position.lineNumber, mouseEvent.target.position.column);
|
||||
|
||||
if (this._dragSelection === null) {
|
||||
let newSelections = this._editor.getSelections().map(selection => {
|
||||
if (selection.containsPosition(newCursorPosition)) {
|
||||
return new Selection(newCursorPosition.lineNumber, newCursorPosition.column, newCursorPosition.lineNumber, newCursorPosition.column);
|
||||
} else {
|
||||
return selection;
|
||||
}
|
||||
});
|
||||
this._editor.setSelections(newSelections);
|
||||
} else if (!this._dragSelection.containsPosition(newCursorPosition) ||
|
||||
(
|
||||
(
|
||||
mouseEvent.event[DragAndDropController.TRIGGER_MODIFIER] ||
|
||||
this._modiferPressed
|
||||
) && (
|
||||
this._dragSelection.getEndPosition().equals(newCursorPosition) || this._dragSelection.getStartPosition().equals(newCursorPosition)
|
||||
) // we allow users to paste content beside the selection
|
||||
)) {
|
||||
this._editor.pushUndoStop();
|
||||
this._editor.executeCommand(DragAndDropController.ID, new DragAndDropCommand(this._dragSelection, newCursorPosition, mouseEvent.event[DragAndDropController.TRIGGER_MODIFIER] || this._modiferPressed));
|
||||
this._editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'text'
|
||||
});
|
||||
|
||||
this._removeDecoration();
|
||||
this._dragSelection = null;
|
||||
this._mouseDown = false;
|
||||
}
|
||||
|
||||
private static _DECORATION_OPTIONS = ModelDecorationOptions.register({
|
||||
className: 'dnd-target'
|
||||
});
|
||||
|
||||
public showAt(position: Position): void {
|
||||
this._editor.changeDecorations(changeAccessor => {
|
||||
let newDecorations: editorCommon.IModelDeltaDecoration[] = [];
|
||||
newDecorations.push({
|
||||
range: new Range(position.lineNumber, position.column, position.lineNumber, position.column),
|
||||
options: DragAndDropController._DECORATION_OPTIONS
|
||||
});
|
||||
|
||||
this._dndDecorationIds = changeAccessor.deltaDecorations(this._dndDecorationIds, newDecorations);
|
||||
});
|
||||
this._editor.revealPosition(position, editorCommon.ScrollType.Immediate);
|
||||
}
|
||||
|
||||
private _removeDecoration(): void {
|
||||
this._editor.changeDecorations(changeAccessor => {
|
||||
changeAccessor.deltaDecorations(this._dndDecorationIds, []);
|
||||
});
|
||||
}
|
||||
|
||||
private _hitContent(target: IMouseTarget): boolean {
|
||||
return target.type === MouseTargetType.CONTENT_TEXT ||
|
||||
target.type === MouseTargetType.CONTENT_EMPTY;
|
||||
}
|
||||
|
||||
private _hitMargin(target: IMouseTarget): boolean {
|
||||
return target.type === MouseTargetType.GUTTER_GLYPH_MARGIN ||
|
||||
target.type === MouseTargetType.GUTTER_LINE_NUMBERS ||
|
||||
target.type === MouseTargetType.GUTTER_LINE_DECORATIONS;
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return DragAndDropController.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._removeDecoration();
|
||||
this._dragSelection = null;
|
||||
this._mouseDown = false;
|
||||
this._modiferPressed = false;
|
||||
this._toUnhook = dispose(this._toUnhook);
|
||||
}
|
||||
}
|
||||
107
src/vs/editor/contrib/dnd/common/dragAndDropCommand.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
|
||||
|
||||
export class DragAndDropCommand implements editorCommon.ICommand {
|
||||
|
||||
private selection: Selection;
|
||||
private targetPosition: Position;
|
||||
private targetSelection: Selection;
|
||||
private copy: boolean;
|
||||
|
||||
constructor(selection: Selection, targetPosition: Position, copy: boolean) {
|
||||
this.selection = selection;
|
||||
this.targetPosition = targetPosition;
|
||||
this.copy = copy;
|
||||
}
|
||||
|
||||
public getEditOperations(model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder): void {
|
||||
let text = model.getValueInRange(this.selection);
|
||||
if (!this.copy) {
|
||||
builder.addEditOperation(this.selection, null);
|
||||
}
|
||||
builder.addEditOperation(new Range(this.targetPosition.lineNumber, this.targetPosition.column, this.targetPosition.lineNumber, this.targetPosition.column), text);
|
||||
|
||||
if (this.selection.containsPosition(this.targetPosition) && !(
|
||||
this.copy && (
|
||||
this.selection.getEndPosition().equals(this.targetPosition) || this.selection.getStartPosition().equals(this.targetPosition)
|
||||
) // we allow users to paste content beside the selection
|
||||
)) {
|
||||
this.targetSelection = this.selection;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.copy) {
|
||||
this.targetSelection = new Selection(
|
||||
this.targetPosition.lineNumber,
|
||||
this.targetPosition.column,
|
||||
this.selection.endLineNumber - this.selection.startLineNumber + this.targetPosition.lineNumber,
|
||||
this.selection.startLineNumber === this.selection.endLineNumber ?
|
||||
this.targetPosition.column + this.selection.endColumn - this.selection.startColumn :
|
||||
this.selection.endColumn
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.targetPosition.lineNumber > this.selection.endLineNumber) {
|
||||
// Drag the selection downwards
|
||||
this.targetSelection = new Selection(
|
||||
this.targetPosition.lineNumber - this.selection.endLineNumber + this.selection.startLineNumber,
|
||||
this.targetPosition.column,
|
||||
this.targetPosition.lineNumber,
|
||||
this.selection.startLineNumber === this.selection.endLineNumber ?
|
||||
this.targetPosition.column + this.selection.endColumn - this.selection.startColumn :
|
||||
this.selection.endColumn
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.targetPosition.lineNumber < this.selection.endLineNumber) {
|
||||
// Drag the selection upwards
|
||||
this.targetSelection = new Selection(
|
||||
this.targetPosition.lineNumber,
|
||||
this.targetPosition.column,
|
||||
this.targetPosition.lineNumber + this.selection.endLineNumber - this.selection.startLineNumber,
|
||||
this.selection.startLineNumber === this.selection.endLineNumber ?
|
||||
this.targetPosition.column + this.selection.endColumn - this.selection.startColumn :
|
||||
this.selection.endColumn
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// The target position is at the same line as the selection's end position.
|
||||
if (this.selection.endColumn <= this.targetPosition.column) {
|
||||
// The target position is after the selection's end position
|
||||
this.targetSelection = new Selection(
|
||||
this.targetPosition.lineNumber - this.selection.endLineNumber + this.selection.startLineNumber,
|
||||
this.selection.startLineNumber === this.selection.endLineNumber ?
|
||||
this.targetPosition.column - this.selection.endColumn + this.selection.startColumn :
|
||||
this.targetPosition.column - this.selection.endColumn + this.selection.startColumn,
|
||||
this.targetPosition.lineNumber,
|
||||
this.selection.startLineNumber === this.selection.endLineNumber ?
|
||||
this.targetPosition.column :
|
||||
this.selection.endColumn
|
||||
);
|
||||
} else {
|
||||
// The target position is before the selection's end postion. Since the selection doesn't contain the target position, the selection is one-line and target position is before this selection.
|
||||
this.targetSelection = new Selection(
|
||||
this.targetPosition.lineNumber - this.selection.endLineNumber + this.selection.startLineNumber,
|
||||
this.targetPosition.column,
|
||||
this.targetPosition.lineNumber,
|
||||
this.targetPosition.column + this.selection.endColumn - this.selection.startColumn
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public computeCursorState(model: editorCommon.ITokenizedModel, helper: editorCommon.ICursorStateComputerData): Selection {
|
||||
return this.targetSelection;
|
||||
}
|
||||
}
|
||||
55
src/vs/editor/contrib/find/browser/find.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
import { FindWidget, IFindController } from 'vs/editor/contrib/find/browser/findWidget';
|
||||
import { FindOptionsWidget } from 'vs/editor/contrib/find/browser/findOptionsWidget';
|
||||
import { CommonFindController, FindStartFocusAction, IFindStartOptions } from 'vs/editor/contrib/find/common/findController';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
@editorContribution
|
||||
export class FindController extends CommonFindController implements IFindController {
|
||||
|
||||
private _widget: FindWidget;
|
||||
private _findOptionsWidget: FindOptionsWidget;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextViewService contextViewService: IContextViewService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IStorageService storageService: IStorageService
|
||||
) {
|
||||
super(editor, contextKeyService, storageService);
|
||||
|
||||
this._widget = this._register(new FindWidget(editor, this, this._state, contextViewService, keybindingService, contextKeyService, themeService));
|
||||
this._findOptionsWidget = this._register(new FindOptionsWidget(editor, this._state, keybindingService, themeService));
|
||||
}
|
||||
|
||||
protected _start(opts: IFindStartOptions): void {
|
||||
super._start(opts);
|
||||
|
||||
if (opts.shouldFocus === FindStartFocusAction.FocusReplaceInput) {
|
||||
this._widget.focusReplaceInput();
|
||||
} else if (opts.shouldFocus === FindStartFocusAction.FocusFindInput) {
|
||||
this._widget.focusFindInput();
|
||||
}
|
||||
}
|
||||
|
||||
public highlightFindOptions(): void {
|
||||
if (this._state.isRevealed) {
|
||||
this._widget.highlightFindOptions();
|
||||
} else {
|
||||
this._findOptionsWidget.highlightFindOptions();
|
||||
}
|
||||
}
|
||||
}
|
||||
207
src/vs/editor/contrib/find/browser/findOptionsWidget.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
|
||||
import { FIND_IDS } from 'vs/editor/contrib/find/common/findModel';
|
||||
import { FindReplaceState } from 'vs/editor/contrib/find/common/findState';
|
||||
import { CaseSensitiveCheckbox, WholeWordsCheckbox, RegexCheckbox } from 'vs/base/browser/ui/findinput/findInputCheckboxes';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IThemeService, ITheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { inputActiveOptionBorder, editorWidgetBackground, contrastBorder, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
export class FindOptionsWidget extends Widget implements IOverlayWidget {
|
||||
|
||||
private static ID = 'editor.contrib.findOptionsWidget';
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _state: FindReplaceState;
|
||||
private _keybindingService: IKeybindingService;
|
||||
|
||||
private _domNode: HTMLElement;
|
||||
private regex: RegexCheckbox;
|
||||
private wholeWords: WholeWordsCheckbox;
|
||||
private caseSensitive: CaseSensitiveCheckbox;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
state: FindReplaceState,
|
||||
keybindingService: IKeybindingService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._editor = editor;
|
||||
this._state = state;
|
||||
this._keybindingService = keybindingService;
|
||||
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = 'findOptionsWidget';
|
||||
this._domNode.style.display = 'none';
|
||||
this._domNode.style.top = '10px';
|
||||
this._domNode.setAttribute('role', 'presentation');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
|
||||
let inputActiveOptionBorderColor = themeService.getTheme().getColor(inputActiveOptionBorder);
|
||||
|
||||
this.caseSensitive = this._register(new CaseSensitiveCheckbox({
|
||||
appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand),
|
||||
isChecked: this._state.matchCase,
|
||||
onChange: (viaKeyboard) => {
|
||||
this._state.change({
|
||||
matchCase: this.caseSensitive.checked
|
||||
}, false);
|
||||
},
|
||||
inputActiveOptionBorder: inputActiveOptionBorderColor
|
||||
}));
|
||||
this._domNode.appendChild(this.caseSensitive.domNode);
|
||||
|
||||
this.wholeWords = this._register(new WholeWordsCheckbox({
|
||||
appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand),
|
||||
isChecked: this._state.wholeWord,
|
||||
onChange: (viaKeyboard) => {
|
||||
this._state.change({
|
||||
wholeWord: this.wholeWords.checked
|
||||
}, false);
|
||||
},
|
||||
inputActiveOptionBorder: inputActiveOptionBorderColor
|
||||
}));
|
||||
this._domNode.appendChild(this.wholeWords.domNode);
|
||||
|
||||
this.regex = this._register(new RegexCheckbox({
|
||||
appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand),
|
||||
isChecked: this._state.isRegex,
|
||||
onChange: (viaKeyboard) => {
|
||||
this._state.change({
|
||||
isRegex: this.regex.checked
|
||||
}, false);
|
||||
},
|
||||
inputActiveOptionBorder: inputActiveOptionBorderColor
|
||||
}));
|
||||
this._domNode.appendChild(this.regex.domNode);
|
||||
|
||||
this._editor.addOverlayWidget(this);
|
||||
|
||||
this._register(this._state.addChangeListener((e) => {
|
||||
let somethingChanged = false;
|
||||
if (e.isRegex) {
|
||||
this.regex.checked = this._state.isRegex;
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (e.wholeWord) {
|
||||
this.wholeWords.checked = this._state.wholeWord;
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (e.matchCase) {
|
||||
this.caseSensitive.checked = this._state.matchCase;
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (!this._state.isRevealed && somethingChanged) {
|
||||
this._revealTemporarily();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(dom.addDisposableNonBubblingMouseOutListener(this._domNode, (e) => this._onMouseOut()));
|
||||
this._register(dom.addDisposableListener(this._domNode, 'mouseover', (e) => this._onMouseOver()));
|
||||
|
||||
this._applyTheme(themeService.getTheme());
|
||||
this._register(themeService.onThemeChange(this._applyTheme.bind(this)));
|
||||
}
|
||||
|
||||
private _keybindingLabelFor(actionId: string): string {
|
||||
let kb = this._keybindingService.lookupKeybinding(actionId);
|
||||
if (!kb) {
|
||||
return '';
|
||||
}
|
||||
return ` (${kb.getLabel()})`;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._editor.removeOverlayWidget(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// ----- IOverlayWidget API
|
||||
|
||||
public getId(): string {
|
||||
return FindOptionsWidget.ID;
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public getPosition(): IOverlayWidgetPosition {
|
||||
return {
|
||||
preference: OverlayWidgetPositionPreference.TOP_RIGHT_CORNER
|
||||
};
|
||||
}
|
||||
|
||||
public highlightFindOptions(): void {
|
||||
this._revealTemporarily();
|
||||
}
|
||||
|
||||
private _hideSoon = this._register(new RunOnceScheduler(() => this._hide(), 1000));
|
||||
|
||||
private _revealTemporarily(): void {
|
||||
this._show();
|
||||
this._hideSoon.schedule();
|
||||
}
|
||||
|
||||
private _onMouseOut(): void {
|
||||
this._hideSoon.schedule();
|
||||
}
|
||||
|
||||
private _onMouseOver(): void {
|
||||
this._hideSoon.cancel();
|
||||
}
|
||||
|
||||
private _isVisible: boolean = false;
|
||||
|
||||
private _show(): void {
|
||||
if (this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = true;
|
||||
this._domNode.style.display = 'block';
|
||||
}
|
||||
|
||||
private _hide(): void {
|
||||
if (!this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = false;
|
||||
this._domNode.style.display = 'none';
|
||||
}
|
||||
|
||||
private _applyTheme(theme: ITheme) {
|
||||
let inputStyles = { inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder) };
|
||||
this.caseSensitive.style(inputStyles);
|
||||
this.wholeWords.style(inputStyles);
|
||||
this.regex.style(inputStyles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let widgetBackground = theme.getColor(editorWidgetBackground);
|
||||
if (widgetBackground) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { background-color: ${widgetBackground}; }`);
|
||||
}
|
||||
|
||||
let widgetShadowColor = theme.getColor(widgetShadow);
|
||||
if (widgetShadowColor) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { box-shadow: 0 2px 8px ${widgetShadowColor}; }`);
|
||||
}
|
||||
|
||||
let hcBorder = theme.getColor(contrastBorder);
|
||||
if (hcBorder) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { border: 2px solid ${hcBorder}; }`);
|
||||
}
|
||||
});
|
||||
356
src/vs/editor/contrib/find/browser/findWidget.css
Normal file
@@ -0,0 +1,356 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Checkbox */
|
||||
|
||||
.monaco-checkbox .label {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid black;
|
||||
background-color: transparent;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-checkbox .checkbox {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.monaco-checkbox .checkbox:checked + .label {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
/* Find widget */
|
||||
.monaco-editor .find-widget {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: -44px; /* find input height + shadow (10px) */
|
||||
height: 34px; /* find input height */
|
||||
overflow: hidden;
|
||||
line-height: 19px;
|
||||
|
||||
-webkit-transition: top 200ms linear;
|
||||
-o-transition: top 200ms linear;
|
||||
-moz-transition: top 200ms linear;
|
||||
-ms-transition: top 200ms linear;
|
||||
transition: top 200ms linear;
|
||||
|
||||
padding: 0 4px;
|
||||
}
|
||||
/* Find widget when replace is toggled on */
|
||||
.monaco-editor .find-widget.replaceToggled {
|
||||
top: -74px; /* find input height + replace input height + shadow (10px) */
|
||||
height: 64px; /* find input height + replace input height */
|
||||
}
|
||||
.monaco-editor .find-widget.replaceToggled > .replace-part {
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.visible,
|
||||
.monaco-editor .find-widget.replaceToggled.visible {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-inputbox .input {
|
||||
background-color: transparent;
|
||||
/* Style to compensate for //winjs */
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .replace-input .input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.visible.noanimation {
|
||||
-webkit-transition: none;
|
||||
-o-transition: none;
|
||||
-moz-transition: none;
|
||||
-ms-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part,
|
||||
.monaco-editor .find-widget > .replace-part {
|
||||
margin: 4px 0 0 17px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part .monaco-inputbox,
|
||||
.monaco-editor .find-widget > .replace-part .monaco-inputbox {
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input {
|
||||
width: 100% !important;
|
||||
padding-right: 66px;
|
||||
}
|
||||
.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input,
|
||||
.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-findInput {
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
flex:1;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .matchesCount {
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
flex: initial;
|
||||
margin: 0 1px 0 3px;
|
||||
padding: 2px 2px 0 2px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
flex: initial;
|
||||
margin-left: 3px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button:not(.disabled):hover {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button.left {
|
||||
margin-left: 0;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button.wide {
|
||||
width: auto;
|
||||
padding: 1px 6px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button.toggle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 18px;
|
||||
height: 100%;
|
||||
-webkit-box-sizing: border-box;
|
||||
-o-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
-ms-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button.toggle.disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .previous {
|
||||
background-image: url('images/previous.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .next {
|
||||
background-image: url('images/next.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .label {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 0;
|
||||
background-image: url('images/cancelSelectionFind.svg');
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .checkbox:disabled + .label {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .checkbox:not(:disabled) + .label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .checkbox:not(:disabled):hover:before + .label {
|
||||
background-color: #DDD;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-checkbox .checkbox:checked + .label {
|
||||
background-color: rgba(100, 100, 100, 0.2);
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .close-fw {
|
||||
background-image: url('images/close.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .expand {
|
||||
background-image: url('images/expando-expanded.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .collapse {
|
||||
background-image: url('images/expando-collapsed.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .replace {
|
||||
background-image: url('images/replace.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .replace-all {
|
||||
background-image: url('images/replace-all.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .replace-part {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .replace-part > .replace-input {
|
||||
display: flex;
|
||||
display: -webkit-flex;
|
||||
vertical-align: middle;
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
/* REDUCED */
|
||||
.monaco-editor .find-widget.reduced-find-widget .matchesCount,
|
||||
.monaco-editor .find-widget.reduced-find-widget .monaco-checkbox {
|
||||
display:none;
|
||||
}
|
||||
|
||||
/* NARROW (SMALLER THAN REDUCED) */
|
||||
.monaco-editor .find-widget.narrow-find-widget {
|
||||
max-width: 257px !important;
|
||||
}
|
||||
|
||||
/* COLLAPSED (SMALLER THAN NARROW) */
|
||||
.monaco-editor .find-widget.collapsed-find-widget {
|
||||
max-width: 111px !important;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.collapsed-find-widget .button.previous,
|
||||
.monaco-editor .find-widget.collapsed-find-widget .button.next,
|
||||
.monaco-editor .find-widget.collapsed-find-widget .button.replace,
|
||||
.monaco-editor .find-widget.collapsed-find-widget .button.replace-all,
|
||||
.monaco-editor .find-widget.collapsed-find-widget > .find-part .monaco-findInput .controls {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.collapsed-find-widget > .find-part .monaco-inputbox > .wrapper > .input {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.monaco-editor .findMatch {
|
||||
-webkit-animation-duration: 0;
|
||||
-webkit-animation-name: inherit !important;
|
||||
-moz-animation-duration: 0;
|
||||
-moz-animation-name: inherit !important;
|
||||
-ms-animation-duration: 0;
|
||||
-ms-animation-name: inherit !important;
|
||||
animation-duration: 0;
|
||||
animation-name: inherit !important;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-sash {
|
||||
width: 2px !important;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .previous,
|
||||
.monaco-editor.vs-dark .find-widget .previous {
|
||||
background-image: url('images/previous-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .next,
|
||||
.monaco-editor.vs-dark .find-widget .next {
|
||||
background-image: url('images/next-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .monaco-checkbox .label,
|
||||
.monaco-editor.vs-dark .find-widget .monaco-checkbox .label {
|
||||
background-image: url('images/cancelSelectionFind-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .find-widget .monaco-checkbox .checkbox:not(:disabled):hover:before + .label {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.monaco-editor.vs-dark .find-widget .monaco-checkbox .checkbox:checked + .label {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .close-fw,
|
||||
.monaco-editor.vs-dark .find-widget .close-fw {
|
||||
background-image: url('images/close-dark.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .replace,
|
||||
.monaco-editor.vs-dark .find-widget .replace {
|
||||
background-image: url('images/replace-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .replace-all,
|
||||
.monaco-editor.vs-dark .find-widget .replace-all {
|
||||
background-image: url('images/replace-all-inverse.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .expand,
|
||||
.monaco-editor.vs-dark .find-widget .expand {
|
||||
background-image: url('images/expando-expanded-dark.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .collapse,
|
||||
.monaco-editor.vs-dark .find-widget .collapse {
|
||||
background-image: url('images/expando-collapsed-dark.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .button:not(.disabled):hover,
|
||||
.monaco-editor.vs-dark .find-widget .button:not(.disabled):hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .button:before {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
left: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .monaco-checkbox .checkbox:checked + .label {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
1079
src/vs/editor/contrib/find/browser/findWidget.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20">
|
||||
<g transform="translate(0,-1032.3622)">
|
||||
<rect width="9" height="2" x="2" y="1046.3622" style="fill:#C5C5C5;fill-opacity:1;stroke:none" />
|
||||
<rect width="13" height="2" x="2" y="1043.3622" style="fill:#C5C5C5;fill-opacity:1;stroke:none" />
|
||||
<rect width="6" height="2" x="2" y="1040.3622" style="fill:#C5C5C5;fill-opacity:1;stroke:none" />
|
||||
<rect width="12" height="2" x="2" y="1037.3622" style="fill:#C5C5C5;fill-opacity:1;stroke:none" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20">
|
||||
<g transform="translate(0,-1032.3622)">
|
||||
<rect width="9" height="2" x="2" y="1046.3622" style="fill:#424242;fill-opacity:1;stroke:none" />
|
||||
<rect width="13" height="2" x="2" y="1043.3622" style="fill:#424242;fill-opacity:1;stroke:none" />
|
||||
<rect width="6" height="2" x="2" y="1040.3622" style="fill:#424242;fill-opacity:1;stroke:none" />
|
||||
<rect width="12" height="2" x="2" y="1037.3622" style="fill:#424242;fill-opacity:1;stroke:none" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 517 B |
1
src/vs/editor/contrib/find/browser/images/close-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#e8e8e8" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
1
src/vs/editor/contrib/find/browser/images/close.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="3 3 16 16" enable-background="new 3 3 16 16"><polygon fill="#424242" points="12.597,11.042 15.4,13.845 13.844,15.4 11.042,12.598 8.239,15.4 6.683,13.845 9.485,11.042 6.683,8.239 8.238,6.683 11.042,9.486 13.845,6.683 15.4,8.239"/></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e8e8e8" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>
|
||||
|
After Width: | Height: | Size: 151 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>
|
||||
|
After Width: | Height: | Size: 151 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#e8e8e8" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>
|
||||
|
After Width: | Height: | Size: 131 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>
|
||||
|
After Width: | Height: | Size: 131 B |
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16" xml:space="preserve">
|
||||
<path fill="#C5C5C5" d="M1,4h7L5,1h3l4,4L8,9H5l3-3H1V4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 290 B |
5
src/vs/editor/contrib/find/browser/images/next.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16" xml:space="preserve">
|
||||
<path fill="#424242" d="M1,4h7L5,1h3l4,4L8,9H5l3-3H1V4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 290 B |
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16" xml:space="preserve">
|
||||
<polygon fill="#C5C5C5" points="13,4 6,4 9,1 6,1 2,5 6,9 9,9 6,6 13,6 "/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 305 B |
5
src/vs/editor/contrib/find/browser/images/previous.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px" width="16px" height="16px" viewBox="-1 -3 16 16" enable-background="new -1 -3 16 16" xml:space="preserve">
|
||||
<polygon fill="#424242" points="13,4 6,4 9,1 6,1 2,5 6,9 9,9 6,6 13,6 "/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 305 B |
@@ -0,0 +1,11 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
|
||||
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
|
||||
<g id="icon_x5F_bg">
|
||||
<path fill="#C5C5C5" d="M11,15V9H1v6H11z M2,14v-2h1v-1H2v-1h3v4H2z M10,11H8v2h2v1H7v-4h3V11z M3,13v-1h1v1H3z M13,7v6h-1V8H5V7
|
||||
H13z M13,2V1h-1v5h3V2H13z M14,5h-1V3h1V5z M11,2v4H8V4h1v1h1V4H9V3H8V2H11z"/>
|
||||
</g>
|
||||
<g id="color_x5F_action">
|
||||
<path fill="#75BEFF" d="M1.979,3.5L2,6L1,5v1.5L2.5,8L4,6.5V5L3,6L2.979,3.5c0-0.275,0.225-0.5,0.5-0.5H7V2H3.479
|
||||
C2.651,2,1.979,2.673,1.979,3.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 637 B |
11
src/vs/editor/contrib/find/browser/images/replace-all.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
|
||||
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
|
||||
<g id="icon_x5F_bg">
|
||||
<path fill="#424242" d="M11,15V9H1v6H11z M2,14v-2h1v-1H2v-1h3v4H2z M10,11H8v2h2v1H7v-4h3V11z M3,13v-1h1v1H3z M13,7v6h-1V8H5V7
|
||||
H13z M13,2V1h-1v5h3V2H13z M14,5h-1V3h1V5z M11,2v4H8V4h1v1h1V4H9V3H8V2H11z"/>
|
||||
</g>
|
||||
<g id="color_x5F_action">
|
||||
<path fill="#00539C" d="M1.979,3.5L2,6L1,5v1.5L2.5,8L4,6.5V5L3,6L2.979,3.5c0-0.275,0.225-0.5,0.5-0.5H7V2H3.479
|
||||
C2.651,2,1.979,2.673,1.979,3.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 637 B |
@@ -0,0 +1,13 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
|
||||
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
|
||||
<g id="icon_x5F_bg">
|
||||
<g>
|
||||
<path fill="#C5C5C5" d="M11,3V1h-1v5v1h1h2h1V4V3H11z M13,6h-2V4h2V6z"/>
|
||||
<path fill="#C5C5C5" d="M2,15h7V9H2V15z M4,10h3v1H5v2h2v1H4V10z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="color_x5F_importance">
|
||||
<path fill="#75BEFF" d="M3.979,3.5L4,6L3,5v1.5L4.5,8L6,6.5V5L5,6L4.979,3.5c0-0.275,0.225-0.5,0.5-0.5H9V2H5.479
|
||||
C4.651,2,3.979,2.673,3.979,3.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 589 B |
13
src/vs/editor/contrib/find/browser/images/replace.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px"
|
||||
height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
|
||||
<g id="icon_x5F_bg">
|
||||
<g>
|
||||
<path fill="#424242" d="M11,3V1h-1v5v1h1h2h1V4V3H11z M13,6h-2V4h2V6z"/>
|
||||
<path fill="#424242" d="M2,15h7V9H2V15z M4,10h3v1H5v2h2v1H4V10z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="color_x5F_importance">
|
||||
<path fill="#00539C" d="M3.979,3.5L4,6L3,5v1.5L4.5,8L6,6.5V5L5,6L4.979,3.5c0-0.275,0.225-0.5,0.5-0.5H9V2H5.479
|
||||
C4.651,2,3.979,2.673,3.979,3.5z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 589 B |
82
src/vs/editor/contrib/find/browser/simpleFindWidget.css
Normal file
@@ -0,0 +1,82 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-workbench .simple-find-part {
|
||||
z-index: 10;
|
||||
position: absolute;
|
||||
top: -40px;
|
||||
right: 28px;
|
||||
display: flex;
|
||||
padding: 4px;
|
||||
align-items: center;
|
||||
width: 220px;
|
||||
max-width: calc(100% - 28px - 28px - 8px);
|
||||
|
||||
-webkit-transition: top 200ms linear;
|
||||
-o-transition: top 200ms linear;
|
||||
-moz-transition: top 200ms linear;
|
||||
-ms-transition: top 200ms linear;
|
||||
transition: top 200ms linear;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part.visible {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .monaco-findInput {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Temporarily we don't show match numbers */
|
||||
.monaco-workbench .simple-find-part .monaco-findInput .controls {
|
||||
display: none;
|
||||
}
|
||||
.monaco-workbench .simple-find-part .monaco-findInput .monaco-inputbox .wrapper .input {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .button {
|
||||
min-width: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
flex: initial;
|
||||
margin-left: 3px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .button.previous {
|
||||
background-image: url('images/previous.svg');
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .button.next {
|
||||
background-image: url('images/next.svg');
|
||||
}
|
||||
|
||||
.monaco-workbench .simple-find-part .button.close-fw {
|
||||
background-image: url('images/close.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .simple-find-part .button.previous,
|
||||
.vs-dark .monaco-workbench .simple-find-part .button.previous {
|
||||
background-image: url('images/previous-inverse.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .simple-find-part .button.next,
|
||||
.vs-dark .monaco-workbench .simple-find-part .button.next {
|
||||
background-image: url('images/next-inverse.svg');
|
||||
}
|
||||
|
||||
.hc-black .monaco-workbench .simple-find-part .button.close-fw,
|
||||
.vs-dark .monaco-workbench .simple-find-part .button.close-fw {
|
||||
background-image: url('images/close-dark.svg');
|
||||
}
|
||||
|
||||
monaco-workbench .simple-find-part .button.disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
223
src/vs/editor/contrib/find/browser/simpleFindWidget.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./simpleFindWidget';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { HistoryNavigator } from 'vs/base/common/history';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { FindInput } from 'vs/base/browser/ui/findinput/findInput';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { registerThemingParticipant, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import { inputBackground, inputActiveOptionBorder, inputForeground, inputBorder, inputValidationInfoBackground, inputValidationInfoBorder, inputValidationWarningBackground, inputValidationWarningBorder, inputValidationErrorBackground, inputValidationErrorBorder, editorWidgetBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { SimpleButton } from './findWidget';
|
||||
|
||||
const NLS_FIND_INPUT_LABEL = nls.localize('label.find', "Find");
|
||||
const NLS_FIND_INPUT_PLACEHOLDER = nls.localize('placeholder.find', "Find");
|
||||
const NLS_PREVIOUS_MATCH_BTN_LABEL = nls.localize('label.previousMatchButton', "Previous match");
|
||||
const NLS_NEXT_MATCH_BTN_LABEL = nls.localize('label.nextMatchButton', "Next match");
|
||||
const NLS_CLOSE_BTN_LABEL = nls.localize('label.closeButton', "Close");
|
||||
|
||||
export abstract class SimpleFindWidget extends Widget {
|
||||
protected _findInput: FindInput;
|
||||
protected _domNode: HTMLElement;
|
||||
protected _isVisible: boolean;
|
||||
protected _focusTracker: dom.IFocusTracker;
|
||||
protected _findInputFocusTracker: dom.IFocusTracker;
|
||||
protected _findHistory: HistoryNavigator<string>;
|
||||
protected _updateHistoryDelayer: Delayer<void>;
|
||||
|
||||
constructor(
|
||||
@IContextViewService private _contextViewService: IContextViewService,
|
||||
private animate: boolean = true
|
||||
) {
|
||||
super();
|
||||
this._findInput = this._register(new FindInput(null, this._contextViewService, {
|
||||
label: NLS_FIND_INPUT_LABEL,
|
||||
placeholder: NLS_FIND_INPUT_PLACEHOLDER,
|
||||
}));
|
||||
|
||||
// Find History with update delayer
|
||||
this._findHistory = new HistoryNavigator<string>();
|
||||
this._updateHistoryDelayer = new Delayer<void>(500);
|
||||
|
||||
this.oninput(this._findInput.domNode, (e) => {
|
||||
this.onInputChanged();
|
||||
this._delayedUpdateHistory();
|
||||
});
|
||||
|
||||
this._register(this._findInput.onKeyDown((e) => {
|
||||
if (e.equals(KeyCode.Enter)) {
|
||||
this.find(false);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.equals(KeyMod.Shift | KeyCode.Enter)) {
|
||||
this.find(true);
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
}));
|
||||
|
||||
let prevBtn = new SimpleButton({
|
||||
label: NLS_PREVIOUS_MATCH_BTN_LABEL,
|
||||
className: 'previous',
|
||||
onTrigger: () => {
|
||||
this.find(true);
|
||||
},
|
||||
onKeyDown: (e) => { }
|
||||
});
|
||||
|
||||
let nextBtn = new SimpleButton({
|
||||
label: NLS_NEXT_MATCH_BTN_LABEL,
|
||||
className: 'next',
|
||||
onTrigger: () => {
|
||||
this.find(false);
|
||||
},
|
||||
onKeyDown: (e) => { }
|
||||
});
|
||||
|
||||
let closeBtn = new SimpleButton({
|
||||
label: NLS_CLOSE_BTN_LABEL,
|
||||
className: 'close-fw',
|
||||
onTrigger: () => {
|
||||
this.hide();
|
||||
},
|
||||
onKeyDown: (e) => { }
|
||||
});
|
||||
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.classList.add('simple-find-part');
|
||||
this._domNode.appendChild(this._findInput.domNode);
|
||||
this._domNode.appendChild(prevBtn.domNode);
|
||||
this._domNode.appendChild(nextBtn.domNode);
|
||||
this._domNode.appendChild(closeBtn.domNode);
|
||||
|
||||
this.onkeyup(this._domNode, e => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
this.hide();
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
this._focusTracker = this._register(dom.trackFocus(this._domNode));
|
||||
this._register(this._focusTracker.addFocusListener(this.onFocusTrackerFocus.bind(this)));
|
||||
this._register(this._focusTracker.addBlurListener(this.onFocusTrackerBlur.bind(this)));
|
||||
|
||||
this._findInputFocusTracker = this._register(dom.trackFocus(this._findInput.domNode));
|
||||
this._register(this._findInputFocusTracker.addFocusListener(this.onFindInputFocusTrackerFocus.bind(this)));
|
||||
this._register(this._findInputFocusTracker.addBlurListener(this.onFindInputFocusTrackerBlur.bind(this)));
|
||||
|
||||
this._register(dom.addDisposableListener(this._domNode, 'click', (event) => {
|
||||
event.stopPropagation();
|
||||
}));
|
||||
}
|
||||
|
||||
protected abstract onInputChanged();
|
||||
protected abstract find(previous: boolean);
|
||||
protected abstract onFocusTrackerFocus();
|
||||
protected abstract onFocusTrackerBlur();
|
||||
protected abstract onFindInputFocusTrackerFocus();
|
||||
protected abstract onFindInputFocusTrackerBlur();
|
||||
|
||||
protected get inputValue() {
|
||||
return this._findInput.getValue();
|
||||
}
|
||||
|
||||
public updateTheme(theme?: ITheme): void {
|
||||
let inputStyles = {
|
||||
inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder),
|
||||
inputBackground: theme.getColor(inputBackground),
|
||||
inputForeground: theme.getColor(inputForeground),
|
||||
inputBorder: theme.getColor(inputBorder),
|
||||
inputValidationInfoBackground: theme.getColor(inputValidationInfoBackground),
|
||||
inputValidationInfoBorder: theme.getColor(inputValidationInfoBorder),
|
||||
inputValidationWarningBackground: theme.getColor(inputValidationWarningBackground),
|
||||
inputValidationWarningBorder: theme.getColor(inputValidationWarningBorder),
|
||||
inputValidationErrorBackground: theme.getColor(inputValidationErrorBackground),
|
||||
inputValidationErrorBorder: theme.getColor(inputValidationErrorBorder)
|
||||
};
|
||||
this._findInput.style(inputStyles);
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public reveal(initialInput?: string): void {
|
||||
if (initialInput) {
|
||||
this._findInput.setValue(initialInput);
|
||||
}
|
||||
|
||||
if (this._isVisible) {
|
||||
this._findInput.select();
|
||||
return;
|
||||
}
|
||||
|
||||
this._isVisible = true;
|
||||
|
||||
setTimeout(() => {
|
||||
dom.addClass(this._domNode, 'visible');
|
||||
this._domNode.setAttribute('aria-hidden', 'false');
|
||||
if (!this.animate) {
|
||||
dom.addClass(this._domNode, 'noanimation');
|
||||
}
|
||||
setTimeout(() => {
|
||||
dom.removeClass(this._domNode, 'noanimation');
|
||||
this._findInput.select();
|
||||
}, 200);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this._isVisible) {
|
||||
this._isVisible = false;
|
||||
|
||||
dom.removeClass(this._domNode, 'visible');
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
protected _delayedUpdateHistory() {
|
||||
this._updateHistoryDelayer.trigger(this._updateHistory.bind(this));
|
||||
}
|
||||
|
||||
protected _updateHistory() {
|
||||
if (this.inputValue) {
|
||||
this._findHistory.add(this._findInput.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
public showNextFindTerm() {
|
||||
let next = this._findHistory.next();
|
||||
if (next) {
|
||||
this._findInput.setValue(next);
|
||||
}
|
||||
}
|
||||
|
||||
public showPreviousFindTerm() {
|
||||
let previous = this._findHistory.previous();
|
||||
if (previous) {
|
||||
this._findInput.setValue(previous);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// theming
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const findWidgetBGColor = theme.getColor(editorWidgetBackground);
|
||||
if (findWidgetBGColor) {
|
||||
collector.addRule(`.monaco-workbench .simple-find-part { background-color: ${findWidgetBGColor} !important; }`);
|
||||
}
|
||||
|
||||
let widgetShadowColor = theme.getColor(widgetShadow);
|
||||
if (widgetShadowColor) {
|
||||
collector.addRule(`.monaco-workbench .simple-find-part { box-shadow: 0 2px 8px ${widgetShadowColor}; }`);
|
||||
}
|
||||
});
|
||||
25
src/vs/editor/contrib/find/common/find.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
|
||||
export function getSelectionSearchString(editor: editorCommon.ICommonCodeEditor): string {
|
||||
let selection = editor.getSelection();
|
||||
|
||||
// if selection spans multiple lines, default search string to empty
|
||||
if (selection.startLineNumber === selection.endLineNumber) {
|
||||
if (selection.isEmpty()) {
|
||||
let wordAtPosition = editor.getModel().getWordAtPosition(selection.getStartPosition());
|
||||
if (wordAtPosition) {
|
||||
return wordAtPosition.word;
|
||||
}
|
||||
} else {
|
||||
return editor.getModel().getValueInRange(selection);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
1277
src/vs/editor/contrib/find/common/findController.ts
Normal file
192
src/vs/editor/contrib/find/common/findDecorations.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations';
|
||||
import { editorFindMatchHighlight, editorFindMatch } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class FindDecorations implements IDisposable {
|
||||
|
||||
private _editor: editorCommon.ICommonCodeEditor;
|
||||
private _decorations: string[];
|
||||
private _findScopeDecorationId: string;
|
||||
private _rangeHighlightDecorationId: string;
|
||||
private _highlightedDecorationId: string;
|
||||
private _startPosition: Position;
|
||||
|
||||
constructor(editor: editorCommon.ICommonCodeEditor) {
|
||||
this._editor = editor;
|
||||
this._decorations = [];
|
||||
this._findScopeDecorationId = null;
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
this._startPosition = this._editor.getPosition();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._editor.deltaDecorations(this._allDecorations(), []);
|
||||
|
||||
this._editor = null;
|
||||
this._decorations = [];
|
||||
this._findScopeDecorationId = null;
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
this._startPosition = null;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._decorations = [];
|
||||
this._findScopeDecorationId = null;
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
}
|
||||
|
||||
public getCount(): number {
|
||||
return this._decorations.length;
|
||||
}
|
||||
|
||||
public getFindScope(): Range {
|
||||
if (this._findScopeDecorationId) {
|
||||
return this._editor.getModel().getDecorationRange(this._findScopeDecorationId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getStartPosition(): Position {
|
||||
return this._startPosition;
|
||||
}
|
||||
|
||||
public setStartPosition(newStartPosition: Position): void {
|
||||
this._startPosition = newStartPosition;
|
||||
this.setCurrentFindMatch(null);
|
||||
}
|
||||
|
||||
public getCurrentMatchesPosition(desiredRange: Range): number {
|
||||
for (let i = 0, len = this._decorations.length; i < len; i++) {
|
||||
let range = this._editor.getModel().getDecorationRange(this._decorations[i]);
|
||||
if (desiredRange.equalsRange(range)) {
|
||||
return (i + 1);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public setCurrentFindMatch(nextMatch: Range): number {
|
||||
let newCurrentDecorationId: string = null;
|
||||
let matchPosition = 0;
|
||||
if (nextMatch) {
|
||||
for (let i = 0, len = this._decorations.length; i < len; i++) {
|
||||
let range = this._editor.getModel().getDecorationRange(this._decorations[i]);
|
||||
if (nextMatch.equalsRange(range)) {
|
||||
newCurrentDecorationId = this._decorations[i];
|
||||
matchPosition = (i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this._highlightedDecorationId !== null || newCurrentDecorationId !== null) {
|
||||
this._editor.changeDecorations((changeAccessor: editorCommon.IModelDecorationsChangeAccessor) => {
|
||||
if (this._highlightedDecorationId !== null) {
|
||||
changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(false));
|
||||
this._highlightedDecorationId = null;
|
||||
}
|
||||
if (newCurrentDecorationId !== null) {
|
||||
this._highlightedDecorationId = newCurrentDecorationId;
|
||||
changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations.createFindMatchDecorationOptions(true));
|
||||
}
|
||||
if (this._rangeHighlightDecorationId !== null) {
|
||||
changeAccessor.removeDecoration(this._rangeHighlightDecorationId);
|
||||
this._rangeHighlightDecorationId = null;
|
||||
}
|
||||
if (newCurrentDecorationId !== null) {
|
||||
let rng = this._editor.getModel().getDecorationRange(newCurrentDecorationId);
|
||||
this._rangeHighlightDecorationId = changeAccessor.addDecoration(rng, FindDecorations._RANGE_HIGHLIGHT_DECORATION);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return matchPosition;
|
||||
}
|
||||
|
||||
public set(matches: Range[], findScope: Range): void {
|
||||
let newDecorations: editorCommon.IModelDeltaDecoration[] = matches.map((match) => {
|
||||
return {
|
||||
range: match,
|
||||
options: FindDecorations.createFindMatchDecorationOptions(false)
|
||||
};
|
||||
});
|
||||
if (findScope) {
|
||||
newDecorations.unshift({
|
||||
range: findScope,
|
||||
options: FindDecorations._FIND_SCOPE_DECORATION
|
||||
});
|
||||
}
|
||||
let tmpDecorations = this._editor.deltaDecorations(this._allDecorations(), newDecorations);
|
||||
|
||||
if (findScope) {
|
||||
this._findScopeDecorationId = tmpDecorations.shift();
|
||||
} else {
|
||||
this._findScopeDecorationId = null;
|
||||
}
|
||||
this._decorations = tmpDecorations;
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
}
|
||||
|
||||
private _allDecorations(): string[] {
|
||||
let result: string[] = [];
|
||||
result = result.concat(this._decorations);
|
||||
if (this._findScopeDecorationId) {
|
||||
result.push(this._findScopeDecorationId);
|
||||
}
|
||||
if (this._rangeHighlightDecorationId) {
|
||||
result.push(this._rangeHighlightDecorationId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static createFindMatchDecorationOptions(isCurrent: boolean): ModelDecorationOptions {
|
||||
return (isCurrent ? this._CURRENT_FIND_MATCH_DECORATION : this._FIND_MATCH_DECORATION);
|
||||
}
|
||||
|
||||
private static _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'currentFindMatch',
|
||||
showIfCollapsed: true,
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(editorFindMatch),
|
||||
darkColor: themeColorFromId(editorFindMatch),
|
||||
position: editorCommon.OverviewRulerLane.Center
|
||||
}
|
||||
});
|
||||
|
||||
private static _FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'findMatch',
|
||||
showIfCollapsed: true,
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(editorFindMatchHighlight),
|
||||
darkColor: themeColorFromId(editorFindMatchHighlight),
|
||||
position: editorCommon.OverviewRulerLane.Center
|
||||
}
|
||||
});
|
||||
|
||||
private static _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'rangeHighlight',
|
||||
isWholeLine: true
|
||||
});
|
||||
|
||||
private static _FIND_SCOPE_DECORATION = ModelDecorationOptions.register({
|
||||
className: 'findScope',
|
||||
isWholeLine: true
|
||||
});
|
||||
}
|
||||
478
src/vs/editor/contrib/find/common/findModel.ts
Normal file
@@ -0,0 +1,478 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ReplacePattern, parseReplaceString } from 'vs/editor/contrib/find/common/replacePattern';
|
||||
import { ReplaceCommand, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { FindDecorations } from './findDecorations';
|
||||
import { FindReplaceState, FindReplaceStateChangedEvent } from './findState';
|
||||
import { ReplaceAllCommand } from './replaceAllCommand';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Constants } from 'vs/editor/common/core/uint';
|
||||
import { SearchParams } from 'vs/editor/common/model/textModelSearch';
|
||||
import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
|
||||
export const ToggleCaseSensitiveKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_C,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_C }
|
||||
};
|
||||
export const ToggleWholeWordKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_W,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_W }
|
||||
};
|
||||
export const ToggleRegexKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_R,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_R }
|
||||
};
|
||||
export const ToggleSearchScopeKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_L,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_L }
|
||||
};
|
||||
export const ShowPreviousFindTermKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.UpArrow
|
||||
};
|
||||
export const ShowNextFindTermKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.DownArrow
|
||||
};
|
||||
|
||||
export const FIND_IDS = {
|
||||
StartFindAction: 'actions.find',
|
||||
NextMatchFindAction: 'editor.action.nextMatchFindAction',
|
||||
PreviousMatchFindAction: 'editor.action.previousMatchFindAction',
|
||||
NextSelectionMatchFindAction: 'editor.action.nextSelectionMatchFindAction',
|
||||
PreviousSelectionMatchFindAction: 'editor.action.previousSelectionMatchFindAction',
|
||||
AddSelectionToNextFindMatchAction: 'editor.action.addSelectionToNextFindMatch',
|
||||
AddSelectionToPreviousFindMatchAction: 'editor.action.addSelectionToPreviousFindMatch',
|
||||
MoveSelectionToNextFindMatchAction: 'editor.action.moveSelectionToNextFindMatch',
|
||||
MoveSelectionToPreviousFindMatchAction: 'editor.action.moveSelectionToPreviousFindMatch',
|
||||
StartFindReplaceAction: 'editor.action.startFindReplaceAction',
|
||||
CloseFindWidgetCommand: 'closeFindWidget',
|
||||
ToggleCaseSensitiveCommand: 'toggleFindCaseSensitive',
|
||||
ToggleWholeWordCommand: 'toggleFindWholeWord',
|
||||
ToggleRegexCommand: 'toggleFindRegex',
|
||||
ToggleSearchScopeCommand: 'toggleFindInSelection',
|
||||
ReplaceOneAction: 'editor.action.replaceOne',
|
||||
ReplaceAllAction: 'editor.action.replaceAll',
|
||||
SelectAllMatchesAction: 'editor.action.selectAllMatches',
|
||||
ShowPreviousFindTermAction: 'find.history.showPrevious',
|
||||
ShowNextFindTermAction: 'find.history.showNext'
|
||||
};
|
||||
|
||||
export const MATCHES_LIMIT = 999;
|
||||
|
||||
export class FindModelBoundToEditorModel {
|
||||
|
||||
private _editor: editorCommon.ICommonCodeEditor;
|
||||
private _state: FindReplaceState;
|
||||
private _toDispose: IDisposable[];
|
||||
private _decorations: FindDecorations;
|
||||
private _ignoreModelContentChanged: boolean;
|
||||
|
||||
private _updateDecorationsScheduler: RunOnceScheduler;
|
||||
private _isDisposed: boolean;
|
||||
|
||||
constructor(editor: editorCommon.ICommonCodeEditor, state: FindReplaceState) {
|
||||
this._editor = editor;
|
||||
this._state = state;
|
||||
this._toDispose = [];
|
||||
this._isDisposed = false;
|
||||
|
||||
this._decorations = new FindDecorations(editor);
|
||||
this._toDispose.push(this._decorations);
|
||||
|
||||
this._updateDecorationsScheduler = new RunOnceScheduler(() => this.research(false), 100);
|
||||
this._toDispose.push(this._updateDecorationsScheduler);
|
||||
|
||||
this._toDispose.push(this._editor.onDidChangeCursorPosition((e: ICursorPositionChangedEvent) => {
|
||||
if (
|
||||
e.reason === CursorChangeReason.Explicit
|
||||
|| e.reason === CursorChangeReason.Undo
|
||||
|| e.reason === CursorChangeReason.Redo
|
||||
) {
|
||||
this._decorations.setStartPosition(this._editor.getPosition());
|
||||
}
|
||||
}));
|
||||
|
||||
this._ignoreModelContentChanged = false;
|
||||
this._toDispose.push(this._editor.onDidChangeModelContent((e) => {
|
||||
if (this._ignoreModelContentChanged) {
|
||||
return;
|
||||
}
|
||||
if (e.isFlush) {
|
||||
// a model.setValue() was called
|
||||
this._decorations.reset();
|
||||
}
|
||||
this._decorations.setStartPosition(this._editor.getPosition());
|
||||
this._updateDecorationsScheduler.schedule();
|
||||
}));
|
||||
|
||||
this._toDispose.push(this._state.addChangeListener((e) => this._onStateChanged(e)));
|
||||
|
||||
this.research(false, this._state.searchScope);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this._toDispose = dispose(this._toDispose);
|
||||
}
|
||||
|
||||
private _onStateChanged(e: FindReplaceStateChangedEvent): void {
|
||||
if (this._isDisposed) {
|
||||
// The find model is disposed during a find state changed event
|
||||
return;
|
||||
}
|
||||
if (e.searchString || e.isReplaceRevealed || e.isRegex || e.wholeWord || e.matchCase || e.searchScope) {
|
||||
if (e.searchScope) {
|
||||
this.research(e.moveCursor, this._state.searchScope);
|
||||
} else {
|
||||
this.research(e.moveCursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static _getSearchRange(model: editorCommon.IModel, searchOnlyEditableRange: boolean, findScope: Range): Range {
|
||||
let searchRange: Range;
|
||||
|
||||
if (searchOnlyEditableRange) {
|
||||
searchRange = model.getEditableRange();
|
||||
} else {
|
||||
searchRange = model.getFullModelRange();
|
||||
}
|
||||
|
||||
// If we have set now or before a find scope, use it for computing the search range
|
||||
if (findScope) {
|
||||
searchRange = searchRange.intersectRanges(findScope);
|
||||
}
|
||||
|
||||
return searchRange;
|
||||
}
|
||||
|
||||
private research(moveCursor: boolean, newFindScope?: Range): void {
|
||||
let findScope: Range = null;
|
||||
if (typeof newFindScope !== 'undefined') {
|
||||
findScope = newFindScope;
|
||||
} else {
|
||||
findScope = this._decorations.getFindScope();
|
||||
}
|
||||
if (findScope !== null) {
|
||||
if (findScope.startLineNumber !== findScope.endLineNumber) {
|
||||
// multiline find scope => expand to line starts / ends
|
||||
findScope = new Range(findScope.startLineNumber, 1, findScope.endLineNumber, this._editor.getModel().getLineMaxColumn(findScope.endLineNumber));
|
||||
}
|
||||
}
|
||||
|
||||
let findMatches = this._findMatches(findScope, false, MATCHES_LIMIT);
|
||||
this._decorations.set(findMatches.map(match => match.range), findScope);
|
||||
|
||||
this._state.changeMatchInfo(
|
||||
this._decorations.getCurrentMatchesPosition(this._editor.getSelection()),
|
||||
this._decorations.getCount(),
|
||||
undefined
|
||||
);
|
||||
|
||||
if (moveCursor) {
|
||||
this._moveToNextMatch(this._decorations.getStartPosition());
|
||||
}
|
||||
}
|
||||
|
||||
private _hasMatches(): boolean {
|
||||
return (this._state.matchesCount > 0);
|
||||
}
|
||||
|
||||
private _cannotFind(): boolean {
|
||||
if (!this._hasMatches()) {
|
||||
let findScope = this._decorations.getFindScope();
|
||||
if (findScope) {
|
||||
// Reveal the selection so user is reminded that 'selection find' is on.
|
||||
this._editor.revealRangeInCenterIfOutsideViewport(findScope, editorCommon.ScrollType.Smooth);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _setCurrentFindMatch(match: Range): void {
|
||||
let matchesPosition = this._decorations.setCurrentFindMatch(match);
|
||||
this._state.changeMatchInfo(
|
||||
matchesPosition,
|
||||
this._decorations.getCount(),
|
||||
match
|
||||
);
|
||||
|
||||
this._editor.setSelection(match);
|
||||
this._editor.revealRangeInCenterIfOutsideViewport(match, editorCommon.ScrollType.Smooth);
|
||||
}
|
||||
|
||||
private _moveToPrevMatch(before: Position, isRecursed: boolean = false): void {
|
||||
if (this._cannotFind()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let findScope = this._decorations.getFindScope();
|
||||
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), this._state.isReplaceRevealed, findScope);
|
||||
|
||||
// ...(----)...|...
|
||||
if (searchRange.getEndPosition().isBefore(before)) {
|
||||
before = searchRange.getEndPosition();
|
||||
}
|
||||
|
||||
// ...|...(----)...
|
||||
if (before.isBefore(searchRange.getStartPosition())) {
|
||||
before = searchRange.getEndPosition();
|
||||
}
|
||||
|
||||
let { lineNumber, column } = before;
|
||||
let model = this._editor.getModel();
|
||||
|
||||
let position = new Position(lineNumber, column);
|
||||
|
||||
let prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null, false);
|
||||
|
||||
if (prevMatch && prevMatch.range.isEmpty() && prevMatch.range.getStartPosition().equals(position)) {
|
||||
// Looks like we're stuck at this position, unacceptable!
|
||||
|
||||
let isUsingLineStops = this._state.isRegex && (
|
||||
this._state.searchString.indexOf('^') >= 0
|
||||
|| this._state.searchString.indexOf('$') >= 0
|
||||
);
|
||||
|
||||
if (isUsingLineStops || column === 1) {
|
||||
if (lineNumber === 1) {
|
||||
lineNumber = model.getLineCount();
|
||||
} else {
|
||||
lineNumber--;
|
||||
}
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
} else {
|
||||
column--;
|
||||
}
|
||||
|
||||
position = new Position(lineNumber, column);
|
||||
prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null, false);
|
||||
}
|
||||
|
||||
if (!prevMatch) {
|
||||
// there is precisely one match and selection is on top of it
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isRecursed && !searchRange.containsRange(prevMatch.range)) {
|
||||
return this._moveToPrevMatch(prevMatch.range.getStartPosition(), true);
|
||||
}
|
||||
|
||||
this._setCurrentFindMatch(prevMatch.range);
|
||||
}
|
||||
|
||||
public moveToPrevMatch(): void {
|
||||
this._moveToPrevMatch(this._editor.getSelection().getStartPosition());
|
||||
}
|
||||
|
||||
private _moveToNextMatch(after: Position): void {
|
||||
let nextMatch = this._getNextMatch(after, false, true);
|
||||
if (nextMatch) {
|
||||
this._setCurrentFindMatch(nextMatch.range);
|
||||
}
|
||||
}
|
||||
|
||||
private _getNextMatch(after: Position, captureMatches: boolean, forceMove: boolean, isRecursed: boolean = false): editorCommon.FindMatch {
|
||||
if (this._cannotFind()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let findScope = this._decorations.getFindScope();
|
||||
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), this._state.isReplaceRevealed, findScope);
|
||||
|
||||
// ...(----)...|...
|
||||
if (searchRange.getEndPosition().isBefore(after)) {
|
||||
after = searchRange.getStartPosition();
|
||||
}
|
||||
|
||||
// ...|...(----)...
|
||||
if (after.isBefore(searchRange.getStartPosition())) {
|
||||
after = searchRange.getStartPosition();
|
||||
}
|
||||
|
||||
let { lineNumber, column } = after;
|
||||
let model = this._editor.getModel();
|
||||
|
||||
let position = new Position(lineNumber, column);
|
||||
|
||||
let nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null, captureMatches);
|
||||
|
||||
if (forceMove && nextMatch && nextMatch.range.isEmpty() && nextMatch.range.getStartPosition().equals(position)) {
|
||||
// Looks like we're stuck at this position, unacceptable!
|
||||
|
||||
let isUsingLineStops = this._state.isRegex && (
|
||||
this._state.searchString.indexOf('^') >= 0
|
||||
|| this._state.searchString.indexOf('$') >= 0
|
||||
);
|
||||
|
||||
if (isUsingLineStops || column === model.getLineMaxColumn(lineNumber)) {
|
||||
if (lineNumber === model.getLineCount()) {
|
||||
lineNumber = 1;
|
||||
} else {
|
||||
lineNumber++;
|
||||
}
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
|
||||
position = new Position(lineNumber, column);
|
||||
nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null, captureMatches);
|
||||
}
|
||||
|
||||
if (!nextMatch) {
|
||||
// there is precisely one match and selection is on top of it
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isRecursed && !searchRange.containsRange(nextMatch.range)) {
|
||||
return this._getNextMatch(nextMatch.range.getEndPosition(), captureMatches, forceMove, true);
|
||||
}
|
||||
|
||||
return nextMatch;
|
||||
}
|
||||
|
||||
public moveToNextMatch(): void {
|
||||
this._moveToNextMatch(this._editor.getSelection().getEndPosition());
|
||||
}
|
||||
|
||||
private _getReplacePattern(): ReplacePattern {
|
||||
if (this._state.isRegex) {
|
||||
return parseReplaceString(this._state.replaceString);
|
||||
}
|
||||
return ReplacePattern.fromStaticValue(this._state.replaceString);
|
||||
}
|
||||
|
||||
public replace(): void {
|
||||
if (!this._hasMatches()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let replacePattern = this._getReplacePattern();
|
||||
let selection = this._editor.getSelection();
|
||||
let nextMatch = this._getNextMatch(selection.getStartPosition(), replacePattern.hasReplacementPatterns, false);
|
||||
if (nextMatch) {
|
||||
if (selection.equalsRange(nextMatch.range)) {
|
||||
// selection sits on a find match => replace it!
|
||||
let replaceString = replacePattern.buildReplaceString(nextMatch.matches);
|
||||
|
||||
let command = new ReplaceCommand(selection, replaceString);
|
||||
|
||||
this._executeEditorCommand('replace', command);
|
||||
|
||||
this._decorations.setStartPosition(new Position(selection.startLineNumber, selection.startColumn + replaceString.length));
|
||||
this.research(true);
|
||||
} else {
|
||||
this._decorations.setStartPosition(this._editor.getPosition());
|
||||
this._setCurrentFindMatch(nextMatch.range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _findMatches(findScope: Range, captureMatches: boolean, limitResultCount: number): editorCommon.FindMatch[] {
|
||||
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), this._state.isReplaceRevealed, findScope);
|
||||
return this._editor.getModel().findMatches(this._state.searchString, searchRange, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null, captureMatches, limitResultCount);
|
||||
}
|
||||
|
||||
public replaceAll(): void {
|
||||
if (!this._hasMatches()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const findScope = this._decorations.getFindScope();
|
||||
|
||||
if (findScope === null && this._state.matchesCount >= MATCHES_LIMIT) {
|
||||
// Doing a replace on the entire file that is over 1k matches
|
||||
this._largeReplaceAll();
|
||||
} else {
|
||||
this._regularReplaceAll(findScope);
|
||||
}
|
||||
|
||||
this.research(false);
|
||||
}
|
||||
|
||||
private _largeReplaceAll(): void {
|
||||
const searchParams = new SearchParams(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getConfiguration().wordSeparators : null);
|
||||
const searchData = searchParams.parseSearchRequest();
|
||||
if (!searchData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const modelText = model.getValue(editorCommon.EndOfLinePreference.LF);
|
||||
const fullModelRange = model.getFullModelRange();
|
||||
|
||||
const replacePattern = this._getReplacePattern();
|
||||
let resultText: string;
|
||||
if (replacePattern.hasReplacementPatterns) {
|
||||
resultText = modelText.replace(searchData.regex, function () {
|
||||
return replacePattern.buildReplaceString(<string[]><any>arguments);
|
||||
});
|
||||
} else {
|
||||
resultText = modelText.replace(searchData.regex, replacePattern.buildReplaceString(null));
|
||||
}
|
||||
|
||||
let command = new ReplaceCommandThatPreservesSelection(fullModelRange, resultText, this._editor.getSelection());
|
||||
this._executeEditorCommand('replaceAll', command);
|
||||
}
|
||||
|
||||
private _regularReplaceAll(findScope: Range): void {
|
||||
const replacePattern = this._getReplacePattern();
|
||||
// Get all the ranges (even more than the highlighted ones)
|
||||
let matches = this._findMatches(findScope, replacePattern.hasReplacementPatterns, Constants.MAX_SAFE_SMALL_INTEGER);
|
||||
|
||||
let replaceStrings: string[] = [];
|
||||
for (let i = 0, len = matches.length; i < len; i++) {
|
||||
replaceStrings[i] = replacePattern.buildReplaceString(matches[i].matches);
|
||||
}
|
||||
|
||||
let command = new ReplaceAllCommand(this._editor.getSelection(), matches.map(m => m.range), replaceStrings);
|
||||
this._executeEditorCommand('replaceAll', command);
|
||||
}
|
||||
|
||||
public selectAllMatches(): void {
|
||||
if (!this._hasMatches()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let findScope = this._decorations.getFindScope();
|
||||
|
||||
// Get all the ranges (even more than the highlighted ones)
|
||||
let matches = this._findMatches(findScope, false, Constants.MAX_SAFE_SMALL_INTEGER);
|
||||
let selections = matches.map(m => new Selection(m.range.startLineNumber, m.range.startColumn, m.range.endLineNumber, m.range.endColumn));
|
||||
|
||||
// If one of the ranges is the editor selection, then maintain it as primary
|
||||
let editorSelection = this._editor.getSelection();
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
let sel = selections[i];
|
||||
if (sel.equalsRange(editorSelection)) {
|
||||
selections = [editorSelection].concat(selections.slice(0, i)).concat(selections.slice(i + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this._editor.setSelections(selections);
|
||||
}
|
||||
|
||||
private _executeEditorCommand(source: string, command: editorCommon.ICommand): void {
|
||||
try {
|
||||
this._ignoreModelContentChanged = true;
|
||||
this._editor.pushUndoStop();
|
||||
this._editor.executeCommand(source, command);
|
||||
this._editor.pushUndoStop();
|
||||
} finally {
|
||||
this._ignoreModelContentChanged = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
219
src/vs/editor/contrib/find/common/findState.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { EventEmitter } from 'vs/base/common/eventEmitter';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
|
||||
export interface FindReplaceStateChangedEvent {
|
||||
moveCursor: boolean;
|
||||
updateHistory: boolean;
|
||||
|
||||
searchString: boolean;
|
||||
replaceString: boolean;
|
||||
isRevealed: boolean;
|
||||
isReplaceRevealed: boolean;
|
||||
isRegex: boolean;
|
||||
wholeWord: boolean;
|
||||
matchCase: boolean;
|
||||
searchScope: boolean;
|
||||
matchesPosition: boolean;
|
||||
matchesCount: boolean;
|
||||
currentMatch: boolean;
|
||||
}
|
||||
|
||||
export interface INewFindReplaceState {
|
||||
searchString?: string;
|
||||
replaceString?: string;
|
||||
isRevealed?: boolean;
|
||||
isReplaceRevealed?: boolean;
|
||||
isRegex?: boolean;
|
||||
wholeWord?: boolean;
|
||||
matchCase?: boolean;
|
||||
searchScope?: Range;
|
||||
}
|
||||
|
||||
export class FindReplaceState implements IDisposable {
|
||||
|
||||
private static _CHANGED_EVENT = 'changed';
|
||||
|
||||
private _searchString: string;
|
||||
private _replaceString: string;
|
||||
private _isRevealed: boolean;
|
||||
private _isReplaceRevealed: boolean;
|
||||
private _isRegex: boolean;
|
||||
private _wholeWord: boolean;
|
||||
private _matchCase: boolean;
|
||||
private _searchScope: Range;
|
||||
private _matchesPosition: number;
|
||||
private _matchesCount: number;
|
||||
private _currentMatch: Range;
|
||||
private _eventEmitter: EventEmitter;
|
||||
|
||||
public get searchString(): string { return this._searchString; }
|
||||
public get replaceString(): string { return this._replaceString; }
|
||||
public get isRevealed(): boolean { return this._isRevealed; }
|
||||
public get isReplaceRevealed(): boolean { return this._isReplaceRevealed; }
|
||||
public get isRegex(): boolean { return this._isRegex; }
|
||||
public get wholeWord(): boolean { return this._wholeWord; }
|
||||
public get matchCase(): boolean { return this._matchCase; }
|
||||
public get searchScope(): Range { return this._searchScope; }
|
||||
public get matchesPosition(): number { return this._matchesPosition; }
|
||||
public get matchesCount(): number { return this._matchesCount; }
|
||||
public get currentMatch(): Range { return this._currentMatch; }
|
||||
|
||||
constructor() {
|
||||
this._searchString = '';
|
||||
this._replaceString = '';
|
||||
this._isRevealed = false;
|
||||
this._isReplaceRevealed = false;
|
||||
this._isRegex = false;
|
||||
this._wholeWord = false;
|
||||
this._matchCase = false;
|
||||
this._searchScope = null;
|
||||
this._matchesPosition = 0;
|
||||
this._matchesCount = 0;
|
||||
this._currentMatch = null;
|
||||
this._eventEmitter = new EventEmitter();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._eventEmitter.dispose();
|
||||
}
|
||||
|
||||
public addChangeListener(listener: (e: FindReplaceStateChangedEvent) => void): IDisposable {
|
||||
return this._eventEmitter.addListener(FindReplaceState._CHANGED_EVENT, listener);
|
||||
}
|
||||
|
||||
public changeMatchInfo(matchesPosition: number, matchesCount: number, currentMatch: Range): void {
|
||||
let changeEvent: FindReplaceStateChangedEvent = {
|
||||
moveCursor: false,
|
||||
updateHistory: false,
|
||||
searchString: false,
|
||||
replaceString: false,
|
||||
isRevealed: false,
|
||||
isReplaceRevealed: false,
|
||||
isRegex: false,
|
||||
wholeWord: false,
|
||||
matchCase: false,
|
||||
searchScope: false,
|
||||
matchesPosition: false,
|
||||
matchesCount: false,
|
||||
currentMatch: false
|
||||
};
|
||||
let somethingChanged = false;
|
||||
|
||||
if (matchesCount === 0) {
|
||||
matchesPosition = 0;
|
||||
}
|
||||
if (matchesPosition > matchesCount) {
|
||||
matchesPosition = matchesCount;
|
||||
}
|
||||
|
||||
if (this._matchesPosition !== matchesPosition) {
|
||||
this._matchesPosition = matchesPosition;
|
||||
changeEvent.matchesPosition = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
if (this._matchesCount !== matchesCount) {
|
||||
this._matchesCount = matchesCount;
|
||||
changeEvent.matchesCount = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
|
||||
if (typeof currentMatch !== 'undefined') {
|
||||
if (!Range.equalsRange(this._currentMatch, currentMatch)) {
|
||||
this._currentMatch = currentMatch;
|
||||
changeEvent.currentMatch = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
this._eventEmitter.emit(FindReplaceState._CHANGED_EVENT, changeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public change(newState: INewFindReplaceState, moveCursor: boolean, updateHistory: boolean = true): void {
|
||||
let changeEvent: FindReplaceStateChangedEvent = {
|
||||
moveCursor: moveCursor,
|
||||
updateHistory: updateHistory,
|
||||
searchString: false,
|
||||
replaceString: false,
|
||||
isRevealed: false,
|
||||
isReplaceRevealed: false,
|
||||
isRegex: false,
|
||||
wholeWord: false,
|
||||
matchCase: false,
|
||||
searchScope: false,
|
||||
matchesPosition: false,
|
||||
matchesCount: false,
|
||||
currentMatch: false
|
||||
};
|
||||
let somethingChanged = false;
|
||||
|
||||
if (typeof newState.searchString !== 'undefined') {
|
||||
if (this._searchString !== newState.searchString) {
|
||||
this._searchString = newState.searchString;
|
||||
changeEvent.searchString = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.replaceString !== 'undefined') {
|
||||
if (this._replaceString !== newState.replaceString) {
|
||||
this._replaceString = newState.replaceString;
|
||||
changeEvent.replaceString = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.isRevealed !== 'undefined') {
|
||||
if (this._isRevealed !== newState.isRevealed) {
|
||||
this._isRevealed = newState.isRevealed;
|
||||
changeEvent.isRevealed = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.isReplaceRevealed !== 'undefined') {
|
||||
if (this._isReplaceRevealed !== newState.isReplaceRevealed) {
|
||||
this._isReplaceRevealed = newState.isReplaceRevealed;
|
||||
changeEvent.isReplaceRevealed = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.isRegex !== 'undefined') {
|
||||
if (this._isRegex !== newState.isRegex) {
|
||||
this._isRegex = newState.isRegex;
|
||||
changeEvent.isRegex = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.wholeWord !== 'undefined') {
|
||||
if (this._wholeWord !== newState.wholeWord) {
|
||||
this._wholeWord = newState.wholeWord;
|
||||
changeEvent.wholeWord = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.matchCase !== 'undefined') {
|
||||
if (this._matchCase !== newState.matchCase) {
|
||||
this._matchCase = newState.matchCase;
|
||||
changeEvent.matchCase = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.searchScope !== 'undefined') {
|
||||
if (!Range.equalsRange(this._searchScope, newState.searchScope)) {
|
||||
this._searchScope = newState.searchScope;
|
||||
changeEvent.searchScope = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
this._eventEmitter.emit(FindReplaceState._CHANGED_EVENT, changeEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/vs/editor/contrib/find/common/replaceAllCommand.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
|
||||
interface IEditOperation {
|
||||
range: Range;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export class ReplaceAllCommand implements editorCommon.ICommand {
|
||||
|
||||
private _editorSelection: Selection;
|
||||
private _trackedEditorSelectionId: string;
|
||||
private _ranges: Range[];
|
||||
private _replaceStrings: string[];
|
||||
|
||||
constructor(editorSelection: Selection, ranges: Range[], replaceStrings: string[]) {
|
||||
this._editorSelection = editorSelection;
|
||||
this._ranges = ranges;
|
||||
this._replaceStrings = replaceStrings;
|
||||
}
|
||||
|
||||
public getEditOperations(model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder): void {
|
||||
if (this._ranges.length > 0) {
|
||||
// Collect all edit operations
|
||||
var ops: IEditOperation[] = [];
|
||||
for (var i = 0; i < this._ranges.length; i++) {
|
||||
ops.push({
|
||||
range: this._ranges[i],
|
||||
text: this._replaceStrings[i]
|
||||
});
|
||||
}
|
||||
|
||||
// Sort them in ascending order by range starts
|
||||
ops.sort((o1, o2) => {
|
||||
return Range.compareRangesUsingStarts(o1.range, o2.range);
|
||||
});
|
||||
|
||||
// Merge operations that touch each other
|
||||
var resultOps: IEditOperation[] = [];
|
||||
var previousOp = ops[0];
|
||||
for (var i = 1; i < ops.length; i++) {
|
||||
if (previousOp.range.endLineNumber === ops[i].range.startLineNumber && previousOp.range.endColumn === ops[i].range.startColumn) {
|
||||
// These operations are one after another and can be merged
|
||||
previousOp.range = previousOp.range.plusRange(ops[i].range);
|
||||
previousOp.text = previousOp.text + ops[i].text;
|
||||
} else {
|
||||
resultOps.push(previousOp);
|
||||
previousOp = ops[i];
|
||||
}
|
||||
}
|
||||
resultOps.push(previousOp);
|
||||
|
||||
for (var i = 0; i < resultOps.length; i++) {
|
||||
builder.addEditOperation(resultOps[i].range, resultOps[i].text);
|
||||
}
|
||||
}
|
||||
|
||||
this._trackedEditorSelectionId = builder.trackSelection(this._editorSelection);
|
||||
}
|
||||
|
||||
public computeCursorState(model: editorCommon.ITokenizedModel, helper: editorCommon.ICursorStateComputerData): Selection {
|
||||
return helper.getTrackedSelection(this._trackedEditorSelectionId);
|
||||
}
|
||||
}
|
||||
266
src/vs/editor/contrib/find/common/replacePattern.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
|
||||
export class ReplacePattern {
|
||||
|
||||
public static fromStaticValue(value: string): ReplacePattern {
|
||||
return new ReplacePattern([ReplacePiece.staticValue(value)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigned when the replace pattern is entirely static.
|
||||
*/
|
||||
private readonly _staticValue: string;
|
||||
|
||||
public get hasReplacementPatterns(): boolean {
|
||||
return this._staticValue === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigned when the replace pattern has replacemend patterns.
|
||||
*/
|
||||
private readonly _pieces: ReplacePiece[];
|
||||
|
||||
constructor(pieces: ReplacePiece[]) {
|
||||
if (!pieces || pieces.length === 0) {
|
||||
this._staticValue = '';
|
||||
this._pieces = null;
|
||||
} else if (pieces.length === 1 && pieces[0].staticValue !== null) {
|
||||
this._staticValue = pieces[0].staticValue;
|
||||
this._pieces = null;
|
||||
} else {
|
||||
this._staticValue = null;
|
||||
this._pieces = pieces;
|
||||
}
|
||||
}
|
||||
|
||||
public buildReplaceString(matches: string[]): string {
|
||||
if (this._staticValue !== null) {
|
||||
return this._staticValue;
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (let i = 0, len = this._pieces.length; i < len; i++) {
|
||||
let piece = this._pieces[i];
|
||||
if (piece.staticValue !== null) {
|
||||
// static value ReplacePiece
|
||||
result += piece.staticValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// match index ReplacePiece
|
||||
result += ReplacePattern._substitute(piece.matchIndex, matches);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _substitute(matchIndex: number, matches: string[]): string {
|
||||
if (matchIndex === 0) {
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
let remainder = '';
|
||||
while (matchIndex > 0) {
|
||||
if (matchIndex < matches.length) {
|
||||
// A match can be undefined
|
||||
let match = (matches[matchIndex] || '');
|
||||
return match + remainder;
|
||||
}
|
||||
remainder = String(matchIndex % 10) + remainder;
|
||||
matchIndex = Math.floor(matchIndex / 10);
|
||||
}
|
||||
return '$' + remainder;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A replace piece can either be a static string or an index to a specific match.
|
||||
*/
|
||||
export class ReplacePiece {
|
||||
|
||||
public static staticValue(value: string): ReplacePiece {
|
||||
return new ReplacePiece(value, -1);
|
||||
}
|
||||
|
||||
public static matchIndex(index: number): ReplacePiece {
|
||||
return new ReplacePiece(null, index);
|
||||
}
|
||||
|
||||
public readonly staticValue: string;
|
||||
public readonly matchIndex: number;
|
||||
|
||||
private constructor(staticValue: string, matchIndex: number) {
|
||||
this.staticValue = staticValue;
|
||||
this.matchIndex = matchIndex;
|
||||
}
|
||||
}
|
||||
|
||||
class ReplacePieceBuilder {
|
||||
|
||||
private readonly _source: string;
|
||||
private _lastCharIndex: number;
|
||||
private readonly _result: ReplacePiece[];
|
||||
private _resultLen: number;
|
||||
private _currentStaticPiece: string;
|
||||
|
||||
constructor(source: string) {
|
||||
this._source = source;
|
||||
this._lastCharIndex = 0;
|
||||
this._result = [];
|
||||
this._resultLen = 0;
|
||||
this._currentStaticPiece = '';
|
||||
}
|
||||
|
||||
public emitUnchanged(toCharIndex: number): void {
|
||||
this._emitStatic(this._source.substring(this._lastCharIndex, toCharIndex));
|
||||
this._lastCharIndex = toCharIndex;
|
||||
}
|
||||
|
||||
public emitStatic(value: string, toCharIndex: number): void {
|
||||
this._emitStatic(value);
|
||||
this._lastCharIndex = toCharIndex;
|
||||
}
|
||||
|
||||
private _emitStatic(value: string): void {
|
||||
if (value.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._currentStaticPiece += value;
|
||||
}
|
||||
|
||||
public emitMatchIndex(index: number, toCharIndex: number): void {
|
||||
if (this._currentStaticPiece.length !== 0) {
|
||||
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
|
||||
this._currentStaticPiece = '';
|
||||
}
|
||||
this._result[this._resultLen++] = ReplacePiece.matchIndex(index);
|
||||
this._lastCharIndex = toCharIndex;
|
||||
}
|
||||
|
||||
|
||||
public finalize(): ReplacePattern {
|
||||
this.emitUnchanged(this._source.length);
|
||||
if (this._currentStaticPiece.length !== 0) {
|
||||
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
|
||||
this._currentStaticPiece = '';
|
||||
}
|
||||
return new ReplacePattern(this._result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* \n => inserts a LF
|
||||
* \t => inserts a TAB
|
||||
* \\ => inserts a "\".
|
||||
* $$ => inserts a "$".
|
||||
* $& and $0 => inserts the matched substring.
|
||||
* $n => Where n is a non-negative integer lesser than 100, inserts the nth parenthesized submatch string
|
||||
* everything else stays untouched
|
||||
*
|
||||
* Also see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace#Specifying_a_string_as_a_parameter
|
||||
*/
|
||||
export function parseReplaceString(replaceString: string): ReplacePattern {
|
||||
if (!replaceString || replaceString.length === 0) {
|
||||
return new ReplacePattern(null);
|
||||
}
|
||||
|
||||
let result = new ReplacePieceBuilder(replaceString);
|
||||
|
||||
for (let i = 0, len = replaceString.length; i < len; i++) {
|
||||
let chCode = replaceString.charCodeAt(i);
|
||||
|
||||
if (chCode === CharCode.Backslash) {
|
||||
|
||||
// move to next char
|
||||
i++;
|
||||
|
||||
if (i >= len) {
|
||||
// string ends with a \
|
||||
break;
|
||||
}
|
||||
|
||||
let nextChCode = replaceString.charCodeAt(i);
|
||||
// let replaceWithCharacter: string = null;
|
||||
|
||||
switch (nextChCode) {
|
||||
case CharCode.Backslash:
|
||||
// \\ => inserts a "\"
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitStatic('\\', i + 1);
|
||||
break;
|
||||
case CharCode.n:
|
||||
// \n => inserts a LF
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitStatic('\n', i + 1);
|
||||
break;
|
||||
case CharCode.t:
|
||||
// \t => inserts a TAB
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitStatic('\t', i + 1);
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chCode === CharCode.DollarSign) {
|
||||
|
||||
// move to next char
|
||||
i++;
|
||||
|
||||
if (i >= len) {
|
||||
// string ends with a $
|
||||
break;
|
||||
}
|
||||
|
||||
let nextChCode = replaceString.charCodeAt(i);
|
||||
|
||||
if (nextChCode === CharCode.DollarSign) {
|
||||
// $$ => inserts a "$"
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitStatic('$', i + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (nextChCode === CharCode.Digit0 || nextChCode === CharCode.Ampersand) {
|
||||
// $& and $0 => inserts the matched substring.
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitMatchIndex(0, i + 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CharCode.Digit1 <= nextChCode && nextChCode <= CharCode.Digit9) {
|
||||
// $n
|
||||
|
||||
let matchIndex = nextChCode - CharCode.Digit0;
|
||||
|
||||
// peek next char to probe for $nn
|
||||
if (i + 1 < len) {
|
||||
let nextNextChCode = replaceString.charCodeAt(i + 1);
|
||||
if (CharCode.Digit0 <= nextNextChCode && nextNextChCode <= CharCode.Digit9) {
|
||||
// $nn
|
||||
|
||||
// move to next char
|
||||
i++;
|
||||
matchIndex = matchIndex * 10 + (nextNextChCode - CharCode.Digit0);
|
||||
|
||||
result.emitUnchanged(i - 2);
|
||||
result.emitMatchIndex(matchIndex, i + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitMatchIndex(matchIndex, i + 1);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.finalize();
|
||||
}
|
||||
89
src/vs/editor/contrib/find/test/common/find.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import {
|
||||
getSelectionSearchString
|
||||
} from 'vs/editor/contrib/find/common/find';
|
||||
import { withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor';
|
||||
|
||||
|
||||
suite('Find', () => {
|
||||
|
||||
test('search string at position', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC DEF',
|
||||
'0123 456'
|
||||
], {}, (editor, cursor) => {
|
||||
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let searchStringAtTop = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringAtTop, 'ABC');
|
||||
|
||||
// Move cursor to the end of ABC
|
||||
editor.setPosition(new Position(1, 3));
|
||||
let searchStringAfterABC = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringAfterABC, 'ABC');
|
||||
|
||||
// Move cursor to DEF
|
||||
editor.setPosition(new Position(1, 5));
|
||||
let searchStringInsideDEF = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringInsideDEF, 'DEF');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
test('search string with selection', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC DEF',
|
||||
'0123 456'
|
||||
], {}, (editor, cursor) => {
|
||||
|
||||
// Select A of ABC
|
||||
editor.setSelection(new Range(1, 1, 1, 2));
|
||||
let searchStringSelectionA = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionA, 'A');
|
||||
|
||||
// Select BC of ABC
|
||||
editor.setSelection(new Range(1, 2, 1, 4));
|
||||
let searchStringSelectionBC = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionBC, 'BC');
|
||||
|
||||
// Select BC DE
|
||||
editor.setSelection(new Range(1, 2, 1, 7));
|
||||
let searchStringSelectionBCDE = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionBCDE, 'BC DE');
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
test('search string with multiline selection', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC DEF',
|
||||
'0123 456'
|
||||
], {}, (editor, cursor) => {
|
||||
|
||||
// Select first line and newline
|
||||
editor.setSelection(new Range(1, 1, 2, 1));
|
||||
let searchStringSelectionWholeLine = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionWholeLine, null);
|
||||
|
||||
// Select first line and chunk of second
|
||||
editor.setSelection(new Range(1, 1, 2, 4));
|
||||
let searchStringSelectionTwoLines = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionTwoLines, null);
|
||||
|
||||
// Select end of first line newline and and chunk of second
|
||||
editor.setSelection(new Range(1, 7, 2, 4));
|
||||
let searchStringSelectionSpanLines = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionSpanLines, null);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
871
src/vs/editor/contrib/find/test/common/findController.test.ts
Normal file
@@ -0,0 +1,871 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { EndOfLineSequence, ICommonCodeEditor, Handler } from 'vs/editor/common/editorCommon';
|
||||
import {
|
||||
CommonFindController, FindStartFocusAction, IFindStartOptions,
|
||||
NextMatchFindAction, StartFindAction, SelectHighlightsAction,
|
||||
AddSelectionToNextFindMatchAction
|
||||
} from 'vs/editor/contrib/find/common/findController';
|
||||
import { MockCodeEditor, withMockCodeEditor } from 'vs/editor/test/common/mocks/mockCodeEditor';
|
||||
import { HistoryNavigator } from 'vs/base/common/history';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
|
||||
class TestFindController extends CommonFindController {
|
||||
|
||||
public hasFocus: boolean;
|
||||
public delayUpdateHistory: boolean = false;
|
||||
public delayedUpdateHistoryPromise: TPromise<void>;
|
||||
|
||||
private _delayedUpdateHistoryEvent: Emitter<void> = new Emitter<void>();
|
||||
|
||||
constructor(editor: ICommonCodeEditor, @IContextKeyService contextKeyService: IContextKeyService, @IStorageService storageService: IStorageService) {
|
||||
super(editor, contextKeyService, storageService);
|
||||
this._updateHistoryDelayer = new Delayer<void>(50);
|
||||
}
|
||||
|
||||
protected _start(opts: IFindStartOptions): void {
|
||||
super._start(opts);
|
||||
|
||||
if (opts.shouldFocus !== FindStartFocusAction.NoFocusChange) {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected _delayedUpdateHistory() {
|
||||
if (!this.delayedUpdateHistoryPromise) {
|
||||
this.delayedUpdateHistoryPromise = new TPromise<void>((c, e) => {
|
||||
const disposable = this._delayedUpdateHistoryEvent.event(() => {
|
||||
disposable.dispose();
|
||||
this.delayedUpdateHistoryPromise = null;
|
||||
c(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (this.delayUpdateHistory) {
|
||||
super._delayedUpdateHistory();
|
||||
} else {
|
||||
this._updateHistory();
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateHistory() {
|
||||
super._updateHistory();
|
||||
this._delayedUpdateHistoryEvent.fire();
|
||||
}
|
||||
}
|
||||
|
||||
function fromRange(rng: Range): number[] {
|
||||
return [rng.startLineNumber, rng.startColumn, rng.endLineNumber, rng.endColumn];
|
||||
}
|
||||
|
||||
suite('FindController', () => {
|
||||
let queryState: { [key: string]: any; } = {};
|
||||
let serviceCollection = new ServiceCollection();
|
||||
serviceCollection.set(IStorageService, <any>{
|
||||
get: (key: string) => queryState[key],
|
||||
getBoolean: (key: string) => !!queryState[key],
|
||||
store: (key: string, value: any) => { queryState[key] = value; }
|
||||
});
|
||||
|
||||
test('issue #1857: F3, Find Next, acts like "Find Under Cursor"', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC',
|
||||
'ABC',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let findState = findController.getState();
|
||||
let startFindAction = new StartFindAction();
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
// I type ABC.
|
||||
findState.change({ searchString: 'A' }, true);
|
||||
findState.change({ searchString: 'AB' }, true);
|
||||
findState.change({ searchString: 'ABC' }, true);
|
||||
|
||||
// The first ABC is highlighted.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 1, 1, 4]);
|
||||
|
||||
// I hit Esc to exit the Find dialog.
|
||||
findController.closeFindWidget();
|
||||
findController.hasFocus = false;
|
||||
|
||||
// The cursor is now at end of the first line, with ABC on that line highlighted.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 1, 1, 4]);
|
||||
|
||||
// I hit delete to remove it and change the text to XYZ.
|
||||
editor.pushUndoStop();
|
||||
editor.executeEdits('test', [EditOperation.delete(new Range(1, 1, 1, 4))]);
|
||||
editor.executeEdits('test', [EditOperation.insert(new Position(1, 1), 'XYZ')]);
|
||||
editor.pushUndoStop();
|
||||
|
||||
// At this point the text editor looks like this:
|
||||
// XYZ
|
||||
// ABC
|
||||
// XYZ
|
||||
// ABC
|
||||
assert.equal(editor.getModel().getLineContent(1), 'XYZ');
|
||||
|
||||
// The cursor is at end of the first line.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 4, 1, 4]);
|
||||
|
||||
// I hit F3 to "Find Next" to find the next occurrence of ABC, but instead it searches for XYZ.
|
||||
nextMatchFindAction.run(null, editor);
|
||||
|
||||
assert.equal(findState.searchString, 'ABC');
|
||||
assert.equal(findController.hasFocus, false);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #3090: F3 does not loop with two matches on a single line', () => {
|
||||
withMockCodeEditor([
|
||||
'import nls = require(\'vs/nls\');'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
editor.setPosition({
|
||||
lineNumber: 1,
|
||||
column: 9
|
||||
});
|
||||
|
||||
nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 26, 1, 29]);
|
||||
|
||||
nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 8, 1, 11]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #6149: Auto-escape highlighted text for search and replace regex mode', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let startFindAction = new StartFindAction();
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
editor.setSelection(new Selection(1, 9, 1, 13));
|
||||
|
||||
findController.toggleRegex();
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 9, 2, 13]);
|
||||
|
||||
nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [1, 9, 1, 13]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #8817: Cursor position changes when you cancel multicursor', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let selectHighlightsAction = new SelectHighlightsAction();
|
||||
|
||||
editor.setSelection(new Selection(2, 9, 2, 16));
|
||||
|
||||
selectHighlightsAction.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[2, 9, 2, 16],
|
||||
[1, 9, 1, 16],
|
||||
[3, 9, 3, 16],
|
||||
]);
|
||||
|
||||
editor.trigger('test', 'removeSecondaryCursors', null);
|
||||
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 9, 2, 16]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #5400: "Select All Occurrences of Find Match" does not select all if find uses regex', () => {
|
||||
withMockCodeEditor([
|
||||
'something',
|
||||
'someething',
|
||||
'someeething',
|
||||
'nothing'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let selectHighlightsAction = new SelectHighlightsAction();
|
||||
|
||||
editor.setSelection(new Selection(1, 1, 1, 1));
|
||||
findController.getState().change({ searchString: 'some+thing', isRegex: true, isRevealed: true }, false);
|
||||
|
||||
selectHighlightsAction.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 1, 1, 10],
|
||||
[2, 1, 2, 11],
|
||||
[3, 1, 3, 12],
|
||||
]);
|
||||
|
||||
assert.equal(findController.getState().searchString, 'some+thing');
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #9043: Clear search scope when find widget is hidden', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: false
|
||||
});
|
||||
|
||||
assert.equal(findController.getState().searchScope, null);
|
||||
|
||||
findController.getState().change({
|
||||
searchScope: new Range(1, 1, 1, 5)
|
||||
}, false);
|
||||
|
||||
assert.deepEqual(findController.getState().searchScope, new Range(1, 1, 1, 5));
|
||||
|
||||
findController.closeFindWidget();
|
||||
assert.equal(findController.getState().searchScope, null);
|
||||
});
|
||||
});
|
||||
|
||||
test('find term is added to history on state change', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
|
||||
assert.deepEqual(['1', '2', '3'], toArray(findController.getHistory()));
|
||||
});
|
||||
});
|
||||
|
||||
test('find term is added with delay', (done) => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.delayUpdateHistory = true;
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
|
||||
findController.delayedUpdateHistoryPromise.then(() => {
|
||||
assert.deepEqual(['3'], toArray(findController.getHistory()));
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('show previous find term', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
|
||||
findController.showPreviousFindTerm();
|
||||
assert.deepEqual('2', findController.getState().searchString);
|
||||
});
|
||||
});
|
||||
|
||||
test('show previous find term do not update history', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
|
||||
findController.showPreviousFindTerm();
|
||||
assert.deepEqual(['1', '2', '3'], toArray(findController.getHistory()));
|
||||
});
|
||||
});
|
||||
|
||||
test('show next find term', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
findController.getState().change({ searchString: '4' }, false);
|
||||
|
||||
findController.showPreviousFindTerm();
|
||||
findController.showPreviousFindTerm();
|
||||
findController.showNextFindTerm();
|
||||
assert.deepEqual('3', findController.getState().searchString);
|
||||
});
|
||||
});
|
||||
|
||||
test('show next find term do not update history', () => {
|
||||
withMockCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.getState().change({ searchString: '1' }, false);
|
||||
findController.getState().change({ searchString: '2' }, false);
|
||||
findController.getState().change({ searchString: '3' }, false);
|
||||
findController.getState().change({ searchString: '4' }, false);
|
||||
|
||||
findController.showPreviousFindTerm();
|
||||
findController.showPreviousFindTerm();
|
||||
findController.showNextFindTerm();
|
||||
assert.deepEqual(['1', '2', '3', '4'], toArray(findController.getHistory()));
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction can work with multiline', () => {
|
||||
withMockCodeEditor([
|
||||
'',
|
||||
'qwe',
|
||||
'rty',
|
||||
'',
|
||||
'qwe',
|
||||
'',
|
||||
'rty',
|
||||
'qwe',
|
||||
'rty'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction();
|
||||
|
||||
editor.setSelection(new Selection(2, 1, 3, 4));
|
||||
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[2, 1, 3, 4],
|
||||
[8, 1, 9, 4]
|
||||
]);
|
||||
|
||||
editor.trigger('test', 'removeSecondaryCursors', null);
|
||||
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 3, 4]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #6661: AddSelectionToNextFindMatchAction can work with touching ranges', () => {
|
||||
withMockCodeEditor([
|
||||
'abcabc',
|
||||
'abc',
|
||||
'abcabc',
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction();
|
||||
|
||||
editor.setSelection(new Selection(1, 1, 1, 4));
|
||||
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 1, 1, 4],
|
||||
[1, 4, 1, 7]
|
||||
]);
|
||||
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 1, 1, 4],
|
||||
[1, 4, 1, 7],
|
||||
[2, 1, 2, 4],
|
||||
[3, 1, 3, 4],
|
||||
[3, 4, 3, 7]
|
||||
]);
|
||||
|
||||
editor.trigger('test', Handler.Type, { text: 'z' });
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 2, 1, 2],
|
||||
[1, 3, 1, 3],
|
||||
[2, 2, 2, 2],
|
||||
[3, 2, 3, 2],
|
||||
[3, 3, 3, 3]
|
||||
]);
|
||||
assert.equal(editor.getValue(), [
|
||||
'zz',
|
||||
'z',
|
||||
'zz',
|
||||
].join('\n'));
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #23541: Multiline Ctrl+D does not work in CRLF files', () => {
|
||||
withMockCodeEditor([
|
||||
'',
|
||||
'qwe',
|
||||
'rty',
|
||||
'',
|
||||
'qwe',
|
||||
'',
|
||||
'rty',
|
||||
'qwe',
|
||||
'rty'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
editor.getModel().setEOL(EndOfLineSequence.CRLF);
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let addSelectionToNextFindMatch = new AddSelectionToNextFindMatchAction();
|
||||
|
||||
editor.setSelection(new Selection(2, 1, 3, 4));
|
||||
|
||||
addSelectionToNextFindMatch.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[2, 1, 3, 4],
|
||||
[8, 1, 9, 4]
|
||||
]);
|
||||
|
||||
editor.trigger('test', 'removeSecondaryCursors', null);
|
||||
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 3, 4]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #18111: Regex replace with single space replaces with no space', () => {
|
||||
withMockCodeEditor([
|
||||
'HRESULT OnAmbientPropertyChange(DISPID dispid);'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
|
||||
let startFindAction = new StartFindAction();
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
findController.getState().change({ searchString: '\\b\\s{3}\\b', replaceString: ' ', isRegex: true }, false);
|
||||
findController.moveToNextMatch();
|
||||
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[1, 39, 1, 42]
|
||||
]);
|
||||
|
||||
findController.replace();
|
||||
|
||||
assert.deepEqual(editor.getValue(), 'HRESULT OnAmbientPropertyChange(DISPID dispid);');
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #24714: Regular expression with ^ in search & replace', () => {
|
||||
withMockCodeEditor([
|
||||
'',
|
||||
'line2',
|
||||
'line3'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
|
||||
let startFindAction = new StartFindAction();
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
findController.getState().change({ searchString: '^', replaceString: 'x', isRegex: true }, false);
|
||||
findController.moveToNextMatch();
|
||||
|
||||
assert.deepEqual(editor.getSelections().map(fromRange), [
|
||||
[2, 1, 2, 1]
|
||||
]);
|
||||
|
||||
findController.replace();
|
||||
|
||||
assert.deepEqual(editor.getValue(), '\nxline2\nline3');
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
function toArray(historyNavigator: HistoryNavigator<string>): string[] {
|
||||
let result = [];
|
||||
historyNavigator.first();
|
||||
if (historyNavigator.current()) {
|
||||
do {
|
||||
result.push(historyNavigator.current());
|
||||
} while (historyNavigator.next());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function testAddSelectionToNextFindMatchAction(text: string[], callback: (editor: MockCodeEditor, action: AddSelectionToNextFindMatchAction, findController: TestFindController) => void): void {
|
||||
withMockCodeEditor(text, { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
|
||||
let action = new AddSelectionToNextFindMatchAction();
|
||||
|
||||
callback(editor, action, findController);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with single collapsed selection', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 2, 1, 2),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with two selections, one being collapsed 1)', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 2, 2, 2),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with two selections, one being collapsed 2)', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 2, 1, 2),
|
||||
new Selection(2, 1, 2, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with all collapsed selections', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 2, 1, 2),
|
||||
new Selection(2, 2, 2, 2),
|
||||
new Selection(3, 1, 3, 1),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 4),
|
||||
new Selection(2, 1, 2, 4),
|
||||
new Selection(3, 1, 3, 4),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('AddSelectionToNextFindMatchAction starting with all collapsed selections on different words', () => {
|
||||
const text = [
|
||||
'abc pizza',
|
||||
'abc house',
|
||||
'abc bar'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 6, 1, 6),
|
||||
new Selection(2, 6, 2, 6),
|
||||
new Selection(3, 6, 3, 6),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 5, 1, 10),
|
||||
new Selection(2, 5, 2, 10),
|
||||
new Selection(3, 5, 3, 8),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 5, 1, 10),
|
||||
new Selection(2, 5, 2, 10),
|
||||
new Selection(3, 5, 3, 8),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #20651: AddSelectionToNextFindMatchAction case insensitive', () => {
|
||||
const text = [
|
||||
'test',
|
||||
'testte',
|
||||
'Test',
|
||||
'testte',
|
||||
'test'
|
||||
];
|
||||
testAddSelectionToNextFindMatchAction(text, (editor, action, findController) => {
|
||||
editor.setSelections([
|
||||
new Selection(1, 1, 1, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
new Selection(3, 1, 3, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
new Selection(3, 1, 3, 5),
|
||||
new Selection(4, 1, 4, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
new Selection(3, 1, 3, 5),
|
||||
new Selection(4, 1, 4, 5),
|
||||
new Selection(5, 1, 5, 5),
|
||||
]);
|
||||
|
||||
action.run(null, editor);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(2, 1, 2, 5),
|
||||
new Selection(3, 1, 3, 5),
|
||||
new Selection(4, 1, 4, 5),
|
||||
new Selection(5, 1, 5, 5),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('FindController query options persistence', () => {
|
||||
let queryState: { [key: string]: any; } = {};
|
||||
queryState['editor.isRegex'] = false;
|
||||
queryState['editor.matchCase'] = false;
|
||||
queryState['editor.wholeWord'] = false;
|
||||
let serviceCollection = new ServiceCollection();
|
||||
serviceCollection.set(IStorageService, <any>{
|
||||
get: (key: string) => queryState[key],
|
||||
getBoolean: (key: string) => !!queryState[key],
|
||||
store: (key: string, value: any) => { queryState[key] = value; }
|
||||
});
|
||||
|
||||
test('matchCase', () => {
|
||||
withMockCodeEditor([
|
||||
'abc',
|
||||
'ABC',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
queryState = { 'editor.isRegex': false, 'editor.matchCase': true, 'editor.wholeWord': false };
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let findState = findController.getState();
|
||||
let startFindAction = new StartFindAction();
|
||||
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
// I type ABC.
|
||||
findState.change({ searchString: 'ABC' }, true);
|
||||
// The second ABC is highlighted as matchCase is true.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 2, 4]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
queryState = { 'editor.isRegex': false, 'editor.matchCase': false, 'editor.wholeWord': true };
|
||||
|
||||
test('wholeWord', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC',
|
||||
'AB',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
queryState = { 'editor.isRegex': false, 'editor.matchCase': false, 'editor.wholeWord': true };
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
let findState = findController.getState();
|
||||
let startFindAction = new StartFindAction();
|
||||
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
// I type AB.
|
||||
findState.change({ searchString: 'AB' }, true);
|
||||
// The second AB is highlighted as wholeWord is true.
|
||||
assert.deepEqual(fromRange(editor.getSelection()), [2, 1, 2, 3]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('toggling options is saved', () => {
|
||||
withMockCodeEditor([
|
||||
'ABC',
|
||||
'AB',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, (editor, cursor) => {
|
||||
queryState = { 'editor.isRegex': false, 'editor.matchCase': false, 'editor.wholeWord': true };
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let findController = editor.registerAndInstantiateContribution<TestFindController>(TestFindController);
|
||||
findController.toggleRegex();
|
||||
assert.equal(queryState['editor.isRegex'], true);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
2037
src/vs/editor/contrib/find/test/common/findModel.test.ts
Normal file
157
src/vs/editor/contrib/find/test/common/replacePattern.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { parseReplaceString, ReplacePattern, ReplacePiece } from 'vs/editor/contrib/find/common/replacePattern';
|
||||
|
||||
suite('Replace Pattern test', () => {
|
||||
|
||||
test('parse replace string', () => {
|
||||
let testParse = (input: string, expectedPieces: ReplacePiece[]) => {
|
||||
let actual = parseReplaceString(input);
|
||||
let expected = new ReplacePattern(expectedPieces);
|
||||
assert.deepEqual(actual, expected, 'Parsing ' + input);
|
||||
};
|
||||
|
||||
// no backslash => no treatment
|
||||
testParse('hello', [ReplacePiece.staticValue('hello')]);
|
||||
|
||||
// \t => TAB
|
||||
testParse('\\thello', [ReplacePiece.staticValue('\thello')]);
|
||||
testParse('h\\tello', [ReplacePiece.staticValue('h\tello')]);
|
||||
testParse('hello\\t', [ReplacePiece.staticValue('hello\t')]);
|
||||
|
||||
// \n => LF
|
||||
testParse('\\nhello', [ReplacePiece.staticValue('\nhello')]);
|
||||
|
||||
// \\t => \t
|
||||
testParse('\\\\thello', [ReplacePiece.staticValue('\\thello')]);
|
||||
testParse('h\\\\tello', [ReplacePiece.staticValue('h\\tello')]);
|
||||
testParse('hello\\\\t', [ReplacePiece.staticValue('hello\\t')]);
|
||||
|
||||
// \\\t => \TAB
|
||||
testParse('\\\\\\thello', [ReplacePiece.staticValue('\\\thello')]);
|
||||
|
||||
// \\\\t => \\t
|
||||
testParse('\\\\\\\\thello', [ReplacePiece.staticValue('\\\\thello')]);
|
||||
|
||||
// \ at the end => no treatment
|
||||
testParse('hello\\', [ReplacePiece.staticValue('hello\\')]);
|
||||
|
||||
// \ with unknown char => no treatment
|
||||
testParse('hello\\x', [ReplacePiece.staticValue('hello\\x')]);
|
||||
|
||||
// \ with back reference => no treatment
|
||||
testParse('hello\\0', [ReplacePiece.staticValue('hello\\0')]);
|
||||
|
||||
testParse('hello$&', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(0)]);
|
||||
testParse('hello$0', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(0)]);
|
||||
testParse('hello$02', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(0), ReplacePiece.staticValue('2')]);
|
||||
testParse('hello$1', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(1)]);
|
||||
testParse('hello$2', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(2)]);
|
||||
testParse('hello$9', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(9)]);
|
||||
testParse('$9hello', [ReplacePiece.matchIndex(9), ReplacePiece.staticValue('hello')]);
|
||||
|
||||
testParse('hello$12', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(12)]);
|
||||
testParse('hello$99', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(99)]);
|
||||
testParse('hello$99a', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(99), ReplacePiece.staticValue('a')]);
|
||||
testParse('hello$1a', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(1), ReplacePiece.staticValue('a')]);
|
||||
testParse('hello$100', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(10), ReplacePiece.staticValue('0')]);
|
||||
testParse('hello$100a', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(10), ReplacePiece.staticValue('0a')]);
|
||||
testParse('hello$10a0', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(10), ReplacePiece.staticValue('a0')]);
|
||||
testParse('hello$$', [ReplacePiece.staticValue('hello$')]);
|
||||
testParse('hello$$0', [ReplacePiece.staticValue('hello$0')]);
|
||||
|
||||
testParse('hello$`', [ReplacePiece.staticValue('hello$`')]);
|
||||
testParse('hello$\'', [ReplacePiece.staticValue('hello$\'')]);
|
||||
});
|
||||
|
||||
test('replace has JavaScript semantics', () => {
|
||||
let testJSReplaceSemantics = (target: string, search: RegExp, replaceString: string, expected: string) => {
|
||||
let replacePattern = parseReplaceString(replaceString);
|
||||
let m = search.exec(target);
|
||||
let actual = replacePattern.buildReplaceString(m);
|
||||
|
||||
assert.deepEqual(actual, expected, `${target}.replace(${search}, ${replaceString})`);
|
||||
};
|
||||
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello', 'hi'.replace(/hi/, 'hello'));
|
||||
testJSReplaceSemantics('hi', /hi/, '\\t', 'hi'.replace(/hi/, '\t'));
|
||||
testJSReplaceSemantics('hi', /hi/, '\\n', 'hi'.replace(/hi/, '\n'));
|
||||
testJSReplaceSemantics('hi', /hi/, '\\\\t', 'hi'.replace(/hi/, '\\t'));
|
||||
testJSReplaceSemantics('hi', /hi/, '\\\\n', 'hi'.replace(/hi/, '\\n'));
|
||||
|
||||
// implicit capture group 0
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello$&', 'hi'.replace(/hi/, 'hello$&'));
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello$0', 'hi'.replace(/hi/, 'hello$&'));
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello$&1', 'hi'.replace(/hi/, 'hello$&1'));
|
||||
testJSReplaceSemantics('hi', /hi/, 'hello$01', 'hi'.replace(/hi/, 'hello$&1'));
|
||||
|
||||
// capture groups have funny semantics in replace strings
|
||||
// the replace string interprets $nn as a captured group only if it exists in the search regex
|
||||
testJSReplaceSemantics('hi', /(hi)/, 'hello$10', 'hi'.replace(/(hi)/, 'hello$10'));
|
||||
testJSReplaceSemantics('hi', /(hi)()()()()()()()()()/, 'hello$10', 'hi'.replace(/(hi)()()()()()()()()()/, 'hello$10'));
|
||||
testJSReplaceSemantics('hi', /(hi)/, 'hello$100', 'hi'.replace(/(hi)/, 'hello$100'));
|
||||
testJSReplaceSemantics('hi', /(hi)/, 'hello$20', 'hi'.replace(/(hi)/, 'hello$20'));
|
||||
});
|
||||
|
||||
test('get replace string if given text is a complete match', () => {
|
||||
function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void {
|
||||
let replacePattern = parseReplaceString(replaceString);
|
||||
let m = search.exec(target);
|
||||
let actual = replacePattern.buildReplaceString(m);
|
||||
|
||||
assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`);
|
||||
}
|
||||
|
||||
assertReplace('bla', /bla/, 'hello', 'hello');
|
||||
assertReplace('bla', /(bla)/, 'hello', 'hello');
|
||||
assertReplace('bla', /(bla)/, 'hello$0', 'hellobla');
|
||||
|
||||
let searchRegex = /let\s+(\w+)\s*=\s*require\s*\(\s*['"]([\w\.\-/]+)\s*['"]\s*\)\s*/;
|
||||
assertReplace('let fs = require(\'fs\')', searchRegex, 'import * as $1 from \'$2\';', 'import * as fs from \'fs\';');
|
||||
assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $1 from \'$2\';', 'import * as something from \'fs\';');
|
||||
assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $1 from \'$1\';', 'import * as something from \'something\';');
|
||||
assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $2 from \'$1\';', 'import * as fs from \'something\';');
|
||||
assertReplace('let something = require(\'fs\')', searchRegex, 'import * as $0 from \'$0\';', 'import * as let something = require(\'fs\') from \'let something = require(\'fs\')\';');
|
||||
assertReplace('let fs = require(\'fs\')', searchRegex, 'import * as $1 from \'$2\';', 'import * as fs from \'fs\';');
|
||||
assertReplace('for ()', /for(.*)/, 'cat$1', 'cat ()');
|
||||
|
||||
// issue #18111
|
||||
assertReplace('HRESULT OnAmbientPropertyChange(DISPID dispid);', /\b\s{3}\b/, ' ', ' ');
|
||||
});
|
||||
|
||||
test('get replace string if match is sub-string of the text', () => {
|
||||
function assertReplace(target: string, search: RegExp, replaceString: string, expected: string): void {
|
||||
let replacePattern = parseReplaceString(replaceString);
|
||||
let m = search.exec(target);
|
||||
let actual = replacePattern.buildReplaceString(m);
|
||||
|
||||
assert.equal(actual, expected, `${target}.replace(${search}, ${replaceString}) === ${expected}`);
|
||||
}
|
||||
assertReplace('this is a bla text', /bla/, 'hello', 'hello');
|
||||
assertReplace('this is a bla text', /this(?=.*bla)/, 'that', 'that');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1at', 'that');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1e', 'the');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1ere', 'there');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$1', 'th');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, 'ma$1', 'math');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, 'ma$1s', 'maths');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$0', 'this');
|
||||
assertReplace('this is a bla text', /(th)is(?=.*bla)/, '$0$1', 'thisth');
|
||||
assertReplace('this is a bla text', /bla(?=\stext$)/, 'foo', 'foo');
|
||||
assertReplace('this is a bla text', /b(la)(?=\stext$)/, 'f$1', 'fla');
|
||||
assertReplace('this is a bla text', /b(la)(?=\stext$)/, 'f$0', 'fbla');
|
||||
assertReplace('this is a bla text', /b(la)(?=\stext$)/, '$0ah', 'blaah');
|
||||
});
|
||||
|
||||
test('issue #19740 Find and replace capture group/backreference inserts `undefined` instead of empty string', () => {
|
||||
let replacePattern = parseReplaceString('a{$1}');
|
||||
let matches = /a(z)?/.exec('abcd');
|
||||
let actual = replacePattern.buildReplaceString(matches);
|
||||
assert.equal(actual, 'a{}');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 15 15" style="enable-background:new 0 0 15 15;">
|
||||
<rect x="3" y="3" style="opacity:0.1;fill:#FFFFFF" width="9" height="9"/>
|
||||
<path style="fill:#5A5A5A" d="M11,4v7H4V4H11 M12,3H3v9h9V3L12,3z"/>
|
||||
<line style="fill:none;stroke:#C5C5C5;stroke-miterlimit:10" x1="10" y1="7.5" x2="5" y2="7.5"/>
|
||||
<line style="fill:none;stroke:#C5C5C5;stroke-miterlimit:10" x1="7.5" y1="5" x2="7.5" y2="10"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 456 B |
6
src/vs/editor/contrib/folding/browser/arrow-collapse.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 15 15" style="enable-background:new 0 0 15 15;">
|
||||
<rect x="3" y="3" style="fill:#E8E8E8" width="9" height="9"/>
|
||||
<path style="fill:#B6B6B6" d="M11,4v7H4V4H11 M12,3H3v9h9V3L12,3z"/>
|
||||
<line style="fill:none;stroke:#6B6B6B;stroke-miterlimit:10" x1="10" y1="7.5" x2="5" y2="7.5"/>
|
||||
<line style="fill:none;stroke:#6B6B6B;stroke-miterlimit:10" x1="7.5" y1="5" x2="7.5" y2="10"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 444 B |
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 15 15" style="enable-background:new 0 0 15 15;">
|
||||
<path style="fill:#5A5A5A" d="M11,4v7H4V4H11 M12,3H3v9h9V3L12,3z"/>
|
||||
<line style="fill:none;stroke:#C5C5C5;stroke-miterlimit:10" x1="10" y1="7.5" x2="5" y2="7.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 287 B |
4
src/vs/editor/contrib/folding/browser/arrow-expand.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 15 15" style="enable-background:new 0 0 15 15;">
|
||||
<path style="fill:#B6B6B6" d="M11,4v7H4V4H11 M12,3H3v9h9V3L12,3z"/>
|
||||
<line style="fill:none;stroke:#6B6B6B;stroke-miterlimit:10" x1="10" y1="7.5" x2="5" y2="7.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 287 B |
49
src/vs/editor/contrib/folding/browser/folding.css
Normal file
@@ -0,0 +1,49 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .margin-view-overlays .folding {
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
background-repeat: no-repeat;
|
||||
background-origin: border-box;
|
||||
background-position: 3px center;
|
||||
background-size: 15px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.monaco-editor .margin-view-overlays .folding {
|
||||
background-image: url('arrow-expand.svg');
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .margin-view-overlays .folding,
|
||||
.monaco-editor.vs-dark .margin-view-overlays .folding {
|
||||
background-image: url('arrow-expand-dark.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .margin-view-overlays:hover .folding,
|
||||
.monaco-editor.alwaysShowFoldIcons .margin-view-overlays .folding {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.monaco-editor .margin-view-overlays .folding.collapsed {
|
||||
background-image: url('arrow-collapse.svg');
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .margin-view-overlays .folding.collapsed,
|
||||
.monaco-editor.vs-dark .margin-view-overlays .folding.collapsed {
|
||||
background-image: url('arrow-collapse-dark.svg');
|
||||
}
|
||||
|
||||
.monaco-editor .inline-folded:after {
|
||||
color: grey;
|
||||
margin: 0.1em 0.2em 0 0.2em;
|
||||
content: "⋯";
|
||||
display: inline;
|
||||
line-height: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
767
src/vs/editor/contrib/folding/browser/folding.ts
Normal file
@@ -0,0 +1,767 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/// <amd-dependency path="vs/css!./folding" />
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { editorAction, ServicesAccessor, EditorAction, CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
import { CollapsibleRegion, getCollapsibleRegionsToFoldAtLine, getCollapsibleRegionsToUnfoldAtLine, doesLineBelongsToCollapsibleRegion, IFoldingRange } from 'vs/editor/contrib/folding/common/foldingModel';
|
||||
import { computeRanges, limitByIndent } from 'vs/editor/contrib/folding/common/indentFoldStrategy';
|
||||
import { IFoldingController, ID } from 'vs/editor/contrib/folding/common/folding';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
@editorContribution
|
||||
export class FoldingController implements IFoldingController {
|
||||
|
||||
static MAX_FOLDING_REGIONS = 5000;
|
||||
|
||||
public static get(editor: editorCommon.ICommonCodeEditor): FoldingController {
|
||||
return editor.getContribution<FoldingController>(ID);
|
||||
}
|
||||
|
||||
private editor: ICodeEditor;
|
||||
private _isEnabled: boolean;
|
||||
private _showFoldingControls: 'always' | 'mouseover';
|
||||
private globalToDispose: IDisposable[];
|
||||
|
||||
private computeToken: number;
|
||||
private cursorChangedScheduler: RunOnceScheduler;
|
||||
private contentChangedScheduler: RunOnceScheduler;
|
||||
private localToDispose: IDisposable[];
|
||||
|
||||
private decorations: CollapsibleRegion[];
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
this.editor = editor;
|
||||
this._isEnabled = this.editor.getConfiguration().contribInfo.folding;
|
||||
this._showFoldingControls = this.editor.getConfiguration().contribInfo.showFoldingControls;
|
||||
|
||||
this.globalToDispose = [];
|
||||
this.localToDispose = [];
|
||||
this.decorations = [];
|
||||
this.computeToken = 0;
|
||||
|
||||
this.globalToDispose.push(this.editor.onDidChangeModel(() => this.onModelChanged()));
|
||||
this.globalToDispose.push(this.editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
|
||||
let oldIsEnabled = this._isEnabled;
|
||||
this._isEnabled = this.editor.getConfiguration().contribInfo.folding;
|
||||
if (oldIsEnabled !== this._isEnabled) {
|
||||
this.onModelChanged();
|
||||
}
|
||||
let oldShowFoldingControls = this._showFoldingControls;
|
||||
this._showFoldingControls = this.editor.getConfiguration().contribInfo.showFoldingControls;
|
||||
if (oldShowFoldingControls !== this._showFoldingControls) {
|
||||
this.updateHideFoldIconClass();
|
||||
}
|
||||
}));
|
||||
|
||||
this.onModelChanged();
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.cleanState();
|
||||
this.globalToDispose = dispose(this.globalToDispose);
|
||||
}
|
||||
|
||||
private updateHideFoldIconClass(): void {
|
||||
let domNode = this.editor.getDomNode();
|
||||
if (domNode) {
|
||||
dom.toggleClass(domNode, 'alwaysShowFoldIcons', this._showFoldingControls === 'always');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store view state.
|
||||
*/
|
||||
public saveViewState(): any {
|
||||
let model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return {};
|
||||
}
|
||||
var collapsedRegions: IFoldingRange[] = [];
|
||||
this.decorations.forEach(d => {
|
||||
if (d.isCollapsed) {
|
||||
var range = d.getDecorationRange(model);
|
||||
if (range) {
|
||||
collapsedRegions.push({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, indent: d.indent, isCollapsed: true });
|
||||
}
|
||||
}
|
||||
});
|
||||
return { collapsedRegions: collapsedRegions, lineCount: model.getLineCount() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore view state.
|
||||
*/
|
||||
public restoreViewState(state: any): void {
|
||||
let model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (!state || !Array.isArray(state.collapsedRegions) || state.collapsedRegions.length === 0 || state.lineCount !== model.getLineCount()) {
|
||||
return;
|
||||
}
|
||||
let newFolded = <IFoldingRange[]>state.collapsedRegions;
|
||||
|
||||
if (this.decorations.length > 0) {
|
||||
let hasChanges = false;
|
||||
let i = 0;
|
||||
this.editor.changeDecorations(changeAccessor => {
|
||||
this.decorations.forEach(d => {
|
||||
if (i === newFolded.length || d.startLineNumber < newFolded[i].startLineNumber) {
|
||||
if (d.isCollapsed) {
|
||||
d.setCollapsed(false, changeAccessor);
|
||||
hasChanges = true;
|
||||
}
|
||||
} else if (d.startLineNumber === newFolded[i].startLineNumber) {
|
||||
if (!d.isCollapsed) {
|
||||
d.setCollapsed(true, changeAccessor);
|
||||
hasChanges = true;
|
||||
}
|
||||
i++;
|
||||
} else {
|
||||
return; // folding regions doesn't match, don't try to restore
|
||||
}
|
||||
});
|
||||
});
|
||||
if (hasChanges) {
|
||||
this.updateHiddenAreas(void 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private cleanState(): void {
|
||||
this.localToDispose = dispose(this.localToDispose);
|
||||
}
|
||||
|
||||
private applyRegions(regions: IFoldingRange[]) {
|
||||
let model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
let updateHiddenRegions = false;
|
||||
regions = limitByIndent(regions, FoldingController.MAX_FOLDING_REGIONS).sort((r1, r2) => r1.startLineNumber - r2.startLineNumber);
|
||||
|
||||
this.editor.changeDecorations(changeAccessor => {
|
||||
|
||||
let newDecorations: CollapsibleRegion[] = [];
|
||||
|
||||
let k = 0, i = 0;
|
||||
while (i < this.decorations.length && k < regions.length) {
|
||||
let dec = this.decorations[i];
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (!decRange) {
|
||||
updateHiddenRegions = updateHiddenRegions || dec.isCollapsed;
|
||||
dec.dispose(changeAccessor);
|
||||
i++;
|
||||
} else {
|
||||
while (k < regions.length && decRange.startLineNumber > regions[k].startLineNumber) {
|
||||
let region = regions[k];
|
||||
updateHiddenRegions = updateHiddenRegions || region.isCollapsed;
|
||||
newDecorations.push(new CollapsibleRegion(region, model, changeAccessor));
|
||||
k++;
|
||||
}
|
||||
if (k < regions.length) {
|
||||
let currRange = regions[k];
|
||||
if (decRange.startLineNumber < currRange.startLineNumber) {
|
||||
updateHiddenRegions = updateHiddenRegions || dec.isCollapsed;
|
||||
dec.dispose(changeAccessor);
|
||||
i++;
|
||||
} else if (decRange.startLineNumber === currRange.startLineNumber) {
|
||||
if (dec.isCollapsed && (dec.startLineNumber !== currRange.startLineNumber || dec.endLineNumber !== currRange.endLineNumber)) {
|
||||
updateHiddenRegions = true;
|
||||
}
|
||||
currRange.isCollapsed = dec.isCollapsed; // preserve collapse state
|
||||
dec.update(currRange, model, changeAccessor);
|
||||
newDecorations.push(dec);
|
||||
i++;
|
||||
k++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
while (i < this.decorations.length) {
|
||||
let dec = this.decorations[i];
|
||||
updateHiddenRegions = updateHiddenRegions || dec.isCollapsed;
|
||||
dec.dispose(changeAccessor);
|
||||
i++;
|
||||
}
|
||||
while (k < regions.length) {
|
||||
let region = regions[k];
|
||||
updateHiddenRegions = updateHiddenRegions || region.isCollapsed;
|
||||
newDecorations.push(new CollapsibleRegion(region, model, changeAccessor));
|
||||
k++;
|
||||
}
|
||||
this.decorations = newDecorations;
|
||||
});
|
||||
if (updateHiddenRegions) {
|
||||
this.updateHiddenAreas();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private onModelChanged(): void {
|
||||
this.cleanState();
|
||||
this.updateHideFoldIconClass();
|
||||
|
||||
let model = this.editor.getModel();
|
||||
if (!this._isEnabled || !model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.computeAndApplyCollapsibleRegions();
|
||||
this.contentChangedScheduler = new RunOnceScheduler(() => this.computeAndApplyCollapsibleRegions(), 200);
|
||||
this.cursorChangedScheduler = new RunOnceScheduler(() => this.revealCursor(), 200);
|
||||
this.localToDispose.push(this.contentChangedScheduler);
|
||||
this.localToDispose.push(this.cursorChangedScheduler);
|
||||
|
||||
this.localToDispose.push(this.editor.onDidChangeModelContent(e => this.contentChangedScheduler.schedule()));
|
||||
this.localToDispose.push(this.editor.onDidChangeCursorPosition((e) => {
|
||||
|
||||
if (!this._isEnabled) {
|
||||
// Early exit if nothing needs to be done!
|
||||
// Leave some form of early exit check here if you wish to continue being a cursor position change listener ;)
|
||||
return;
|
||||
}
|
||||
|
||||
this.cursorChangedScheduler.schedule();
|
||||
}));
|
||||
this.localToDispose.push(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));
|
||||
this.localToDispose.push(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));
|
||||
|
||||
this.localToDispose.push({ dispose: () => this.disposeDecorations() });
|
||||
}
|
||||
|
||||
private computeAndApplyCollapsibleRegions(): void {
|
||||
let model = this.editor.getModel();
|
||||
this.applyRegions(model ? computeRanges(model) : []);
|
||||
}
|
||||
|
||||
private disposeDecorations() {
|
||||
this.editor.changeDecorations(changeAccessor => {
|
||||
this.decorations.forEach(dec => dec.dispose(changeAccessor));
|
||||
});
|
||||
this.decorations = [];
|
||||
this.editor.setHiddenAreas([]);
|
||||
}
|
||||
|
||||
private revealCursor() {
|
||||
let model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
let hasChanges = false;
|
||||
let selections = this.editor.getSelections();
|
||||
|
||||
this.editor.changeDecorations(changeAccessor => {
|
||||
return this.decorations.forEach(dec => {
|
||||
if (dec.isCollapsed) {
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (decRange) {
|
||||
for (let selection of selections) {
|
||||
// reveal if cursor in in one of the collapsed line (not the first)
|
||||
if (decRange.startLineNumber < selection.selectionStartLineNumber && selection.selectionStartLineNumber <= decRange.endLineNumber) {
|
||||
dec.setCollapsed(false, changeAccessor);
|
||||
hasChanges = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (hasChanges) {
|
||||
this.updateHiddenAreas(this.editor.getPosition().lineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
private mouseDownInfo: { lineNumber: number, iconClicked: boolean };
|
||||
|
||||
private onEditorMouseDown(e: IEditorMouseEvent): void {
|
||||
this.mouseDownInfo = null;
|
||||
|
||||
if (this.decorations.length === 0) {
|
||||
return;
|
||||
}
|
||||
let range = e.target.range;
|
||||
if (!range) {
|
||||
return;
|
||||
}
|
||||
if (!e.event.leftButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = this.editor.getModel();
|
||||
|
||||
let iconClicked = false;
|
||||
switch (e.target.type) {
|
||||
case MouseTargetType.GUTTER_LINE_DECORATIONS:
|
||||
iconClicked = true;
|
||||
break;
|
||||
case MouseTargetType.CONTENT_EMPTY:
|
||||
case MouseTargetType.CONTENT_TEXT:
|
||||
if (range.startColumn === model.getLineMaxColumn(range.startLineNumber)) {
|
||||
break;
|
||||
}
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
this.mouseDownInfo = { lineNumber: range.startLineNumber, iconClicked };
|
||||
}
|
||||
|
||||
private onEditorMouseUp(e: IEditorMouseEvent): void {
|
||||
if (!this.mouseDownInfo) {
|
||||
return;
|
||||
}
|
||||
let lineNumber = this.mouseDownInfo.lineNumber;
|
||||
let iconClicked = this.mouseDownInfo.iconClicked;
|
||||
|
||||
let range = e.target.range;
|
||||
if (!range || range.startLineNumber !== lineNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = this.editor.getModel();
|
||||
|
||||
if (iconClicked) {
|
||||
if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (range.startColumn !== model.getLineMaxColumn(lineNumber)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.editor.changeDecorations(changeAccessor => {
|
||||
for (let i = 0; i < this.decorations.length; i++) {
|
||||
let dec = this.decorations[i];
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (decRange && decRange.startLineNumber === lineNumber) {
|
||||
if (iconClicked || dec.isCollapsed) {
|
||||
dec.setCollapsed(!dec.isCollapsed, changeAccessor);
|
||||
this.updateHiddenAreas(lineNumber);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private updateHiddenAreas(focusLine?: number): void {
|
||||
let model = this.editor.getModel();
|
||||
var selections: Selection[] = this.editor.getSelections();
|
||||
var updateSelections = false;
|
||||
let hiddenAreas: Range[] = [];
|
||||
this.decorations.filter(dec => dec.isCollapsed).forEach(dec => {
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (!decRange) {
|
||||
return;
|
||||
}
|
||||
let isLineHidden = line => line > decRange.startLineNumber && line <= decRange.endLineNumber;
|
||||
hiddenAreas.push(new Range(decRange.startLineNumber + 1, 1, decRange.endLineNumber, 1));
|
||||
selections.forEach((selection, i) => {
|
||||
if (isLineHidden(selection.getStartPosition().lineNumber)) {
|
||||
selections[i] = selection = selection.setStartPosition(decRange.startLineNumber, model.getLineMaxColumn(decRange.startLineNumber));
|
||||
updateSelections = true;
|
||||
}
|
||||
if (isLineHidden(selection.getEndPosition().lineNumber)) {
|
||||
selections[i] = selection.setEndPosition(decRange.startLineNumber, model.getLineMaxColumn(decRange.startLineNumber));
|
||||
updateSelections = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (updateSelections) {
|
||||
this.editor.setSelections(selections);
|
||||
}
|
||||
this.editor.setHiddenAreas(hiddenAreas);
|
||||
if (focusLine) {
|
||||
this.editor.revealPositionInCenterIfOutsideViewport({ lineNumber: focusLine, column: 1 }, editorCommon.ScrollType.Smooth);
|
||||
}
|
||||
}
|
||||
|
||||
public unfold(levels: number): void {
|
||||
let model = this.editor.getModel();
|
||||
let hasChanges = false;
|
||||
let selections = this.editor.getSelections();
|
||||
let selectionsHasChanged = false;
|
||||
selections.forEach((selection, index) => {
|
||||
let toUnfold: CollapsibleRegion[] = getCollapsibleRegionsToUnfoldAtLine(this.decorations, model, selection.startLineNumber, levels);
|
||||
if (toUnfold.length > 0) {
|
||||
toUnfold.forEach((collapsibleRegion, index) => {
|
||||
this.editor.changeDecorations(changeAccessor => {
|
||||
collapsibleRegion.setCollapsed(false, changeAccessor);
|
||||
hasChanges = true;
|
||||
});
|
||||
});
|
||||
if (!doesLineBelongsToCollapsibleRegion(toUnfold[0].foldingRange, selection.startLineNumber)) {
|
||||
let lineNumber = toUnfold[0].startLineNumber, column = model.getLineMaxColumn(toUnfold[0].startLineNumber);
|
||||
selections[index] = selection.setEndPosition(lineNumber, column).setStartPosition(lineNumber, column);
|
||||
selectionsHasChanged = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (selectionsHasChanged) {
|
||||
this.editor.setSelections(selections);
|
||||
}
|
||||
if (hasChanges) {
|
||||
this.updateHiddenAreas(selections[0].startLineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public fold(levels: number, up: boolean): void {
|
||||
let hasChanges = false;
|
||||
let selections = this.editor.getSelections();
|
||||
selections.forEach(selection => {
|
||||
let lineNumber = selection.startLineNumber;
|
||||
let toFold: CollapsibleRegion[] = getCollapsibleRegionsToFoldAtLine(this.decorations, this.editor.getModel(), lineNumber, levels, up);
|
||||
toFold.forEach(collapsibleRegion => this.editor.changeDecorations(changeAccessor => {
|
||||
collapsibleRegion.setCollapsed(true, changeAccessor);
|
||||
hasChanges = true;
|
||||
}));
|
||||
});
|
||||
if (hasChanges) {
|
||||
this.updateHiddenAreas(selections[0].startLineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public foldUnfoldRecursively(isFold: boolean): void {
|
||||
let hasChanges = false;
|
||||
let model = this.editor.getModel();
|
||||
let selections = this.editor.getSelections();
|
||||
selections.forEach(selection => {
|
||||
let lineNumber = selection.startLineNumber;
|
||||
let endLineNumber: number;
|
||||
let decToFoldUnfold: CollapsibleRegion[] = [];
|
||||
for (let i = 0, len = this.decorations.length; i < len; i++) {
|
||||
let dec = this.decorations[i];
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (!decRange) {
|
||||
continue;
|
||||
}
|
||||
if (decRange.startLineNumber >= lineNumber && (decRange.endLineNumber <= endLineNumber || typeof endLineNumber === 'undefined')) {
|
||||
//Protect against cursor not being in decoration and lower decoration folding/unfolding
|
||||
if (decRange.startLineNumber !== lineNumber && typeof endLineNumber === 'undefined') {
|
||||
return;
|
||||
}
|
||||
endLineNumber = endLineNumber || decRange.endLineNumber;
|
||||
decToFoldUnfold.push(dec);
|
||||
}
|
||||
};
|
||||
if (decToFoldUnfold.length > 0) {
|
||||
decToFoldUnfold.forEach(dec => {
|
||||
this.editor.changeDecorations(changeAccessor => {
|
||||
dec.setCollapsed(isFold, changeAccessor);
|
||||
hasChanges = true;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
if (hasChanges) {
|
||||
this.updateHiddenAreas(selections[0].startLineNumber);
|
||||
}
|
||||
}
|
||||
|
||||
public foldAll(): void {
|
||||
this.changeAll(true);
|
||||
}
|
||||
|
||||
public unfoldAll(): void {
|
||||
this.changeAll(false);
|
||||
}
|
||||
|
||||
private changeAll(collapse: boolean): void {
|
||||
if (this.decorations.length > 0) {
|
||||
let hasChanges = true;
|
||||
this.editor.changeDecorations(changeAccessor => {
|
||||
this.decorations.forEach(d => {
|
||||
if (collapse !== d.isCollapsed) {
|
||||
d.setCollapsed(collapse, changeAccessor);
|
||||
hasChanges = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (hasChanges) {
|
||||
this.updateHiddenAreas(this.editor.getPosition().lineNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public foldLevel(foldLevel: number, selectedLineNumbers: number[]): void {
|
||||
let model = this.editor.getModel();
|
||||
let foldingRegionStack: Range[] = [model.getFullModelRange()]; // sentinel
|
||||
|
||||
let hasChanges = false;
|
||||
this.editor.changeDecorations(changeAccessor => {
|
||||
this.decorations.forEach(dec => {
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (decRange) {
|
||||
while (!Range.containsRange(foldingRegionStack[foldingRegionStack.length - 1], decRange)) {
|
||||
foldingRegionStack.pop();
|
||||
}
|
||||
foldingRegionStack.push(decRange);
|
||||
if (foldingRegionStack.length === foldLevel + 1 && !dec.isCollapsed && !selectedLineNumbers.some(lineNumber => decRange.startLineNumber < lineNumber && lineNumber <= decRange.endLineNumber)) {
|
||||
dec.setCollapsed(true, changeAccessor);
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if (hasChanges) {
|
||||
this.updateHiddenAreas(selectedLineNumbers[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FoldingAction<T> extends EditorAction {
|
||||
|
||||
abstract invoke(foldingController: FoldingController, editor: editorCommon.ICommonCodeEditor, args: T): void;
|
||||
|
||||
public runEditorCommand(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor, args: T): void | TPromise<void> {
|
||||
let foldingController = FoldingController.get(editor);
|
||||
if (!foldingController) {
|
||||
return;
|
||||
}
|
||||
this.reportTelemetry(accessor, editor);
|
||||
this.invoke(foldingController, editor, args);
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
interface FoldingArguments {
|
||||
levels?: number;
|
||||
direction?: 'up' | 'down';
|
||||
}
|
||||
|
||||
function foldingArgumentsConstraint(args: any) {
|
||||
if (!types.isUndefined(args)) {
|
||||
if (!types.isObject(args)) {
|
||||
return false;
|
||||
}
|
||||
const foldingArgs: FoldingArguments = args;
|
||||
if (!types.isUndefined(foldingArgs.levels) && !types.isNumber(foldingArgs.levels)) {
|
||||
return false;
|
||||
}
|
||||
if (!types.isUndefined(foldingArgs.direction) && !types.isString(foldingArgs.direction)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class UnfoldAction extends FoldingAction<FoldingArguments> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.unfold',
|
||||
label: nls.localize('unfoldAction.label', "Unfold"),
|
||||
alias: 'Unfold',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET
|
||||
}
|
||||
},
|
||||
description: {
|
||||
description: 'Unfold the content in the editor',
|
||||
args: [
|
||||
{
|
||||
name: 'Unfold editor argument',
|
||||
description: `Property-value pairs that can be passed through this argument:
|
||||
* 'level': Number of levels to unfold
|
||||
`,
|
||||
constraint: foldingArgumentsConstraint
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(foldingController: FoldingController, editor: editorCommon.ICommonCodeEditor, args: FoldingArguments): void {
|
||||
foldingController.unfold(args ? args.levels || 1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class UnFoldRecursivelyAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.unfoldRecursively',
|
||||
label: nls.localize('unFoldRecursivelyAction.label', "Unfold Recursively"),
|
||||
alias: 'Unfold Recursively',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_CLOSE_SQUARE_BRACKET)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(foldingController: FoldingController, editor: editorCommon.ICommonCodeEditor, args: any): void {
|
||||
foldingController.foldUnfoldRecursively(false);
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class FoldAction extends FoldingAction<FoldingArguments> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.fold',
|
||||
label: nls.localize('foldAction.label', "Fold"),
|
||||
alias: 'Fold',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET
|
||||
}
|
||||
},
|
||||
description: {
|
||||
description: 'Fold the content in the editor',
|
||||
args: [
|
||||
{
|
||||
name: 'Fold editor argument',
|
||||
description: `Property-value pairs that can be passed through this argument:
|
||||
* 'levels': Number of levels to fold
|
||||
* 'up': If 'true', folds given number of levels up otherwise folds down
|
||||
`,
|
||||
constraint: foldingArgumentsConstraint
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(foldingController: FoldingController, editor: editorCommon.ICommonCodeEditor, args: FoldingArguments): void {
|
||||
args = args ? args : { levels: 1, direction: 'up' };
|
||||
foldingController.fold(args.levels || 1, args.direction === 'up');
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class FoldRecursivelyAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.foldRecursively',
|
||||
label: nls.localize('foldRecursivelyAction.label', "Fold Recursively"),
|
||||
alias: 'Fold Recursively',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_OPEN_SQUARE_BRACKET)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(foldingController: FoldingController, editor: editorCommon.ICommonCodeEditor): void {
|
||||
foldingController.foldUnfoldRecursively(true);
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class FoldAllAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.foldAll',
|
||||
label: nls.localize('foldAllAction.label', "Fold All"),
|
||||
alias: 'Fold All',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_0)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(foldingController: FoldingController, editor: editorCommon.ICommonCodeEditor): void {
|
||||
foldingController.foldAll();
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class UnfoldAllAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.unfoldAll',
|
||||
label: nls.localize('unfoldAllAction.label', "Unfold All"),
|
||||
alias: 'Unfold All',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_J)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(foldingController: FoldingController, editor: editorCommon.ICommonCodeEditor): void {
|
||||
foldingController.unfoldAll();
|
||||
}
|
||||
}
|
||||
|
||||
class FoldLevelAction extends FoldingAction<void> {
|
||||
private static ID_PREFIX = 'editor.foldLevel';
|
||||
public static ID = (level: number) => FoldLevelAction.ID_PREFIX + level;
|
||||
|
||||
private getFoldingLevel() {
|
||||
return parseInt(this.id.substr(FoldLevelAction.ID_PREFIX.length));
|
||||
}
|
||||
|
||||
private getSelectedLines(editor: editorCommon.ICommonCodeEditor) {
|
||||
return editor.getSelections().map(s => s.startLineNumber);
|
||||
}
|
||||
|
||||
invoke(foldingController: FoldingController, editor: editorCommon.ICommonCodeEditor): void {
|
||||
foldingController.foldLevel(this.getFoldingLevel(), this.getSelectedLines(editor));
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
CommonEditorRegistry.registerEditorAction(
|
||||
new FoldLevelAction({
|
||||
id: FoldLevelAction.ID(i),
|
||||
label: nls.localize('foldLevelAction.label', "Fold Level {0}", i),
|
||||
alias: `Fold Level ${i}`,
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | (KeyCode.KEY_0 + i))
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
15
src/vs/editor/contrib/folding/common/folding.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
|
||||
export const ID = 'editor.contrib.folding';
|
||||
|
||||
export interface IFoldingController extends IEditorContribution {
|
||||
|
||||
foldAll(): void;
|
||||
unfoldAll(): void;
|
||||
|
||||
}
|
||||
295
src/vs/editor/contrib/folding/common/foldingModel.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations';
|
||||
|
||||
export interface IFoldingRange {
|
||||
startLineNumber: number;
|
||||
endLineNumber: number;
|
||||
indent: number;
|
||||
isCollapsed?: boolean;
|
||||
}
|
||||
|
||||
export function toString(range: IFoldingRange): string {
|
||||
return (range ? range.startLineNumber + '/' + range.endLineNumber : 'null') + (range.isCollapsed ? ' (collapsed)' : '') + ' - ' + range.indent;
|
||||
}
|
||||
|
||||
export class CollapsibleRegion {
|
||||
|
||||
private decorationIds: string[];
|
||||
private _isCollapsed: boolean;
|
||||
private _indent: number;
|
||||
|
||||
private _lastRange: IFoldingRange;
|
||||
|
||||
public constructor(range: IFoldingRange, model: editorCommon.IModel, changeAccessor: editorCommon.IModelDecorationsChangeAccessor) {
|
||||
this.decorationIds = [];
|
||||
this.update(range, model, changeAccessor);
|
||||
}
|
||||
|
||||
public get isCollapsed(): boolean {
|
||||
return this._isCollapsed;
|
||||
}
|
||||
|
||||
public get isExpanded(): boolean {
|
||||
return !this._isCollapsed;
|
||||
}
|
||||
|
||||
public get indent(): number {
|
||||
return this._indent;
|
||||
}
|
||||
|
||||
public get foldingRange(): IFoldingRange {
|
||||
return this._lastRange;
|
||||
}
|
||||
|
||||
public get startLineNumber(): number {
|
||||
return this._lastRange ? this._lastRange.startLineNumber : void 0;
|
||||
}
|
||||
|
||||
public get endLineNumber(): number {
|
||||
return this._lastRange ? this._lastRange.endLineNumber : void 0;
|
||||
}
|
||||
|
||||
public setCollapsed(isCollaped: boolean, changeAccessor: editorCommon.IModelDecorationsChangeAccessor): void {
|
||||
this._isCollapsed = isCollaped;
|
||||
if (this.decorationIds.length > 0) {
|
||||
changeAccessor.changeDecorationOptions(this.decorationIds[0], this.getVisualDecorationOptions());
|
||||
}
|
||||
}
|
||||
|
||||
public getDecorationRange(model: editorCommon.IModel): Range {
|
||||
if (this.decorationIds.length > 0) {
|
||||
return model.getDecorationRange(this.decorationIds[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static _COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
afterContentClassName: 'inline-folded',
|
||||
linesDecorationsClassName: 'folding collapsed'
|
||||
});
|
||||
|
||||
private static _EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
linesDecorationsClassName: 'folding'
|
||||
});
|
||||
|
||||
private getVisualDecorationOptions(): ModelDecorationOptions {
|
||||
if (this._isCollapsed) {
|
||||
return CollapsibleRegion._COLLAPSED_VISUAL_DECORATION;
|
||||
} else {
|
||||
return CollapsibleRegion._EXPANDED_VISUAL_DECORATION;
|
||||
}
|
||||
}
|
||||
|
||||
private static _RANGE_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: editorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore
|
||||
});
|
||||
|
||||
private getRangeDecorationOptions(): ModelDecorationOptions {
|
||||
return CollapsibleRegion._RANGE_DECORATION;
|
||||
}
|
||||
|
||||
public update(newRange: IFoldingRange, model: editorCommon.IModel, changeAccessor: editorCommon.IModelDecorationsChangeAccessor): void {
|
||||
this._lastRange = newRange;
|
||||
this._isCollapsed = !!newRange.isCollapsed;
|
||||
this._indent = newRange.indent;
|
||||
|
||||
let newDecorations: editorCommon.IModelDeltaDecoration[] = [];
|
||||
|
||||
let maxColumn = model.getLineMaxColumn(newRange.startLineNumber);
|
||||
let visualRng = {
|
||||
startLineNumber: newRange.startLineNumber,
|
||||
startColumn: maxColumn,
|
||||
endLineNumber: newRange.startLineNumber,
|
||||
endColumn: maxColumn
|
||||
};
|
||||
newDecorations.push({ range: visualRng, options: this.getVisualDecorationOptions() });
|
||||
|
||||
let colRng = {
|
||||
startLineNumber: newRange.startLineNumber,
|
||||
startColumn: 1,
|
||||
endLineNumber: newRange.endLineNumber,
|
||||
endColumn: model.getLineMaxColumn(newRange.endLineNumber)
|
||||
};
|
||||
newDecorations.push({ range: colRng, options: this.getRangeDecorationOptions() });
|
||||
|
||||
this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, newDecorations);
|
||||
}
|
||||
|
||||
|
||||
public dispose(changeAccessor: editorCommon.IModelDecorationsChangeAccessor): void {
|
||||
this._lastRange = null;
|
||||
this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, []);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
let str = this.isCollapsed ? 'collapsed ' : 'expanded ';
|
||||
if (this._lastRange) {
|
||||
str += (this._lastRange.startLineNumber + '/' + this._lastRange.endLineNumber);
|
||||
} else {
|
||||
str += 'no range';
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCollapsibleRegionsToFoldAtLine(allRegions: CollapsibleRegion[], model: editorCommon.IModel, lineNumber: number, levels: number, up: boolean): CollapsibleRegion[] {
|
||||
let surroundingRegion: CollapsibleRegion = getCollapsibleRegionAtLine(allRegions, model, lineNumber);
|
||||
if (!surroundingRegion) {
|
||||
return [];
|
||||
}
|
||||
if (levels === 1) {
|
||||
return [surroundingRegion];
|
||||
}
|
||||
let result = getCollapsibleRegionsFor(surroundingRegion, allRegions, model, levels, up);
|
||||
return result.filter(collapsibleRegion => !collapsibleRegion.isCollapsed);
|
||||
}
|
||||
|
||||
export function getCollapsibleRegionsToUnfoldAtLine(allRegions: CollapsibleRegion[], model: editorCommon.IModel, lineNumber: number, levels: number): CollapsibleRegion[] {
|
||||
let surroundingRegion: CollapsibleRegion = getCollapsibleRegionAtLine(allRegions, model, lineNumber);
|
||||
if (!surroundingRegion) {
|
||||
return [];
|
||||
}
|
||||
if (levels === 1) {
|
||||
let regionToUnfold = surroundingRegion.isCollapsed ? surroundingRegion : getFoldedCollapsibleRegionAfterLine(allRegions, model, surroundingRegion, lineNumber);
|
||||
return regionToUnfold ? [regionToUnfold] : [];
|
||||
}
|
||||
let result = getCollapsibleRegionsFor(surroundingRegion, allRegions, model, levels, false);
|
||||
return result.filter(collapsibleRegion => collapsibleRegion.isCollapsed);
|
||||
}
|
||||
|
||||
function getCollapsibleRegionAtLine(allRegions: CollapsibleRegion[], model: editorCommon.IModel, lineNumber: number): CollapsibleRegion {
|
||||
let collapsibleRegion: CollapsibleRegion = null;
|
||||
for (let i = 0, len = allRegions.length; i < len; i++) {
|
||||
let dec = allRegions[i];
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (decRange) {
|
||||
if (doesLineBelongsToCollapsibleRegion(decRange, lineNumber)) {
|
||||
collapsibleRegion = dec;
|
||||
}
|
||||
if (doesCollapsibleRegionIsAfterLine(decRange, lineNumber)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return collapsibleRegion;
|
||||
}
|
||||
|
||||
function getFoldedCollapsibleRegionAfterLine(allRegions: CollapsibleRegion[], model: editorCommon.IModel, surroundingRegion: CollapsibleRegion, lineNumber: number): CollapsibleRegion {
|
||||
let index = allRegions.indexOf(surroundingRegion);
|
||||
for (let i = index + 1; i < allRegions.length; i++) {
|
||||
let dec = allRegions[i];
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (decRange) {
|
||||
if (doesCollapsibleRegionIsAfterLine(decRange, lineNumber)) {
|
||||
if (!doesCollapsibleRegionContains(surroundingRegion.foldingRange, decRange)) {
|
||||
return null;
|
||||
}
|
||||
if (dec.isCollapsed) {
|
||||
return dec;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function doesLineBelongsToCollapsibleRegion(range: IFoldingRange | Range, lineNumber: number): boolean {
|
||||
return lineNumber >= range.startLineNumber && lineNumber <= range.endLineNumber;
|
||||
}
|
||||
|
||||
function doesCollapsibleRegionIsAfterLine(range: IFoldingRange | Range, lineNumber: number): boolean {
|
||||
return lineNumber < range.startLineNumber;
|
||||
}
|
||||
function doesCollapsibleRegionIsBeforeLine(range: IFoldingRange | Range, lineNumber: number): boolean {
|
||||
return lineNumber > range.endLineNumber;
|
||||
}
|
||||
|
||||
function doesCollapsibleRegionContains(range1: IFoldingRange | Range, range2: IFoldingRange | Range): boolean {
|
||||
if (range1 instanceof Range && range2 instanceof Range) {
|
||||
return range1.containsRange(range2);
|
||||
}
|
||||
return range1.startLineNumber <= range2.startLineNumber && range1.endLineNumber >= range2.endLineNumber;
|
||||
}
|
||||
|
||||
function getCollapsibleRegionsFor(surroundingRegion: CollapsibleRegion, allRegions: CollapsibleRegion[], model: editorCommon.IModel, levels: number, up: boolean): CollapsibleRegion[] {
|
||||
let collapsibleRegionsHierarchy: CollapsibleRegionsHierarchy = up ? new CollapsibleRegionsParentHierarchy(surroundingRegion, allRegions, model) : new CollapsibleRegionsChildrenHierarchy(surroundingRegion, allRegions, model);
|
||||
return collapsibleRegionsHierarchy.getRegionsTill(levels);
|
||||
}
|
||||
|
||||
interface CollapsibleRegionsHierarchy {
|
||||
getRegionsTill(level: number): CollapsibleRegion[];
|
||||
}
|
||||
|
||||
class CollapsibleRegionsChildrenHierarchy implements CollapsibleRegionsHierarchy {
|
||||
|
||||
children: CollapsibleRegionsChildrenHierarchy[] = [];
|
||||
lastChildIndex: number;
|
||||
|
||||
constructor(private region: CollapsibleRegion, allRegions: CollapsibleRegion[], model: editorCommon.IModel) {
|
||||
for (let index = allRegions.indexOf(region) + 1; index < allRegions.length; index++) {
|
||||
let dec = allRegions[index];
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (decRange) {
|
||||
if (doesCollapsibleRegionContains(region.foldingRange, decRange)) {
|
||||
index = this.processChildRegion(dec, allRegions, model, index);
|
||||
}
|
||||
if (doesCollapsibleRegionIsAfterLine(decRange, region.foldingRange.endLineNumber)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processChildRegion(dec: CollapsibleRegion, allRegions: CollapsibleRegion[], model: editorCommon.IModel, index: number): number {
|
||||
let childRegion = new CollapsibleRegionsChildrenHierarchy(dec, allRegions, model);
|
||||
this.children.push(childRegion);
|
||||
this.lastChildIndex = index;
|
||||
return childRegion.children.length > 0 ? childRegion.lastChildIndex : index;
|
||||
}
|
||||
|
||||
public getRegionsTill(level: number): CollapsibleRegion[] {
|
||||
let result = [this.region];
|
||||
if (level > 1) {
|
||||
this.children.forEach(region => result = result.concat(region.getRegionsTill(level - 1)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
class CollapsibleRegionsParentHierarchy implements CollapsibleRegionsHierarchy {
|
||||
|
||||
parent: CollapsibleRegionsParentHierarchy;
|
||||
lastChildIndex: number;
|
||||
|
||||
constructor(private region: CollapsibleRegion, allRegions: CollapsibleRegion[], model: editorCommon.IModel) {
|
||||
for (let index = allRegions.indexOf(region) - 1; index >= 0; index--) {
|
||||
let dec = allRegions[index];
|
||||
let decRange = dec.getDecorationRange(model);
|
||||
if (decRange) {
|
||||
if (doesCollapsibleRegionContains(decRange, region.foldingRange)) {
|
||||
this.parent = new CollapsibleRegionsParentHierarchy(dec, allRegions, model);
|
||||
break;
|
||||
}
|
||||
if (doesCollapsibleRegionIsBeforeLine(decRange, region.foldingRange.endLineNumber)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getRegionsTill(level: number): CollapsibleRegion[] {
|
||||
let result = [this.region];
|
||||
if (this.parent && level > 1) {
|
||||
result = result.concat(this.parent.getRegionsTill(level - 1));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
42
src/vs/editor/contrib/folding/common/indentFoldStrategy.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { IModel } from 'vs/editor/common/editorCommon';
|
||||
import { IFoldingRange } from 'vs/editor/contrib/folding/common/foldingModel';
|
||||
|
||||
export function computeRanges(model: IModel): IFoldingRange[] {
|
||||
// we get here a clone of the model's indent ranges
|
||||
return model.getIndentRanges();
|
||||
}
|
||||
|
||||
/**
|
||||
* Limits the number of folding ranges by removing ranges with larger indent levels
|
||||
*/
|
||||
export function limitByIndent(ranges: IFoldingRange[], maxEntries: number): IFoldingRange[] {
|
||||
if (ranges.length <= maxEntries) {
|
||||
return ranges;
|
||||
}
|
||||
|
||||
let indentOccurrences: number[] = [];
|
||||
ranges.forEach(r => {
|
||||
if (r.indent < 1000) {
|
||||
indentOccurrences[r.indent] = (indentOccurrences[r.indent] || 0) + 1;
|
||||
}
|
||||
});
|
||||
let maxIndent = indentOccurrences.length;
|
||||
for (let i = 0; i < indentOccurrences.length; i++) {
|
||||
if (indentOccurrences[i]) {
|
||||
maxEntries -= indentOccurrences[i];
|
||||
if (maxEntries < 0) {
|
||||
maxIndent = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return ranges.filter(r => r.indent < maxIndent);
|
||||
}
|
||||
29
src/vs/editor/contrib/folding/test/indentFold.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IFoldingRange } from 'vs/editor/contrib/folding/common/foldingModel';
|
||||
import { limitByIndent } from 'vs/editor/contrib/folding/common/indentFoldStrategy';
|
||||
|
||||
suite('Indentation Folding', () => {
|
||||
function r(startLineNumber: number, endLineNumber: number, indent: number): IFoldingRange {
|
||||
return { startLineNumber, endLineNumber, indent };
|
||||
}
|
||||
|
||||
test('Limit By indent', () => {
|
||||
let ranges = [r(1, 4, 0), r(3, 4, 2), r(5, 8, 0), r(6, 7, 1), r(9, 15, 0), r(10, 15, 10), r(11, 12, 2000), r(14, 15, 2000)];
|
||||
assert.deepEqual(limitByIndent(ranges, 8), [r(1, 4, 0), r(3, 4, 2), r(5, 8, 0), r(6, 7, 1), r(9, 15, 0), r(10, 15, 10), r(11, 12, 2000), r(14, 15, 2000)]);
|
||||
assert.deepEqual(limitByIndent(ranges, 7), [r(1, 4, 0), r(3, 4, 2), r(5, 8, 0), r(6, 7, 1), r(9, 15, 0), r(10, 15, 10)]);
|
||||
assert.deepEqual(limitByIndent(ranges, 6), [r(1, 4, 0), r(3, 4, 2), r(5, 8, 0), r(6, 7, 1), r(9, 15, 0), r(10, 15, 10)]);
|
||||
assert.deepEqual(limitByIndent(ranges, 5), [r(1, 4, 0), r(3, 4, 2), r(5, 8, 0), r(6, 7, 1), r(9, 15, 0)]);
|
||||
assert.deepEqual(limitByIndent(ranges, 4), [r(1, 4, 0), r(5, 8, 0), r(6, 7, 1), r(9, 15, 0)]);
|
||||
assert.deepEqual(limitByIndent(ranges, 3), [r(1, 4, 0), r(5, 8, 0), r(9, 15, 0)]);
|
||||
assert.deepEqual(limitByIndent(ranges, 2), []);
|
||||
assert.deepEqual(limitByIndent(ranges, 1), []);
|
||||
assert.deepEqual(limitByIndent(ranges, 0), []);
|
||||
});
|
||||
|
||||
});
|
||||
368
src/vs/editor/contrib/format/browser/formatActions.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
|
||||
import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { editorAction, ServicesAccessor, EditorAction, commonEditorContribution } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { OnTypeFormattingEditProviderRegistry, DocumentRangeFormattingEditProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { getOnTypeFormattingEdits, getDocumentFormattingEdits, getDocumentRangeFormattingEdits } from '../common/format';
|
||||
import { EditOperationsCommand } from '../common/formatCommand';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { CharacterSet } from 'vs/editor/common/core/characterClassifier';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { EditorState, CodeEditorStateFlag } from 'vs/editor/common/core/editorState';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
|
||||
|
||||
function alertFormattingEdits(edits: editorCommon.ISingleEditOperation[]): void {
|
||||
|
||||
edits = edits.filter(edit => edit.range);
|
||||
if (!edits.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let { range } = edits[0];
|
||||
for (let i = 1; i < edits.length; i++) {
|
||||
range = Range.plusRange(range, edits[i].range);
|
||||
}
|
||||
const { startLineNumber, endLineNumber } = range;
|
||||
if (startLineNumber === endLineNumber) {
|
||||
if (edits.length === 1) {
|
||||
alert(nls.localize('hint11', "Made 1 formatting edit on line {0}", startLineNumber));
|
||||
} else {
|
||||
alert(nls.localize('hintn1', "Made {0} formatting edits on line {1}", edits.length, startLineNumber));
|
||||
}
|
||||
} else {
|
||||
if (edits.length === 1) {
|
||||
alert(nls.localize('hint1n', "Made 1 formatting edit between lines {0} and {1}", startLineNumber, endLineNumber));
|
||||
} else {
|
||||
alert(nls.localize('hintnn', "Made {0} formatting edits between lines {1} and {2}", edits.length, startLineNumber, endLineNumber));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@commonEditorContribution
|
||||
class FormatOnType implements editorCommon.IEditorContribution {
|
||||
|
||||
private static ID = 'editor.contrib.autoFormat';
|
||||
|
||||
private editor: editorCommon.ICommonCodeEditor;
|
||||
private workerService: IEditorWorkerService;
|
||||
private callOnDispose: IDisposable[];
|
||||
private callOnModel: IDisposable[];
|
||||
|
||||
constructor(editor: editorCommon.ICommonCodeEditor, @IEditorWorkerService workerService: IEditorWorkerService) {
|
||||
this.editor = editor;
|
||||
this.workerService = workerService;
|
||||
this.callOnDispose = [];
|
||||
this.callOnModel = [];
|
||||
|
||||
this.callOnDispose.push(editor.onDidChangeConfiguration(() => this.update()));
|
||||
this.callOnDispose.push(editor.onDidChangeModel(() => this.update()));
|
||||
this.callOnDispose.push(editor.onDidChangeModelLanguage(() => this.update()));
|
||||
this.callOnDispose.push(OnTypeFormattingEditProviderRegistry.onDidChange(this.update, this));
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
|
||||
// clean up
|
||||
this.callOnModel = dispose(this.callOnModel);
|
||||
|
||||
// we are disabled
|
||||
if (!this.editor.getConfiguration().contribInfo.formatOnType) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no model
|
||||
if (!this.editor.getModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var model = this.editor.getModel();
|
||||
|
||||
// no support
|
||||
var [support] = OnTypeFormattingEditProviderRegistry.ordered(model);
|
||||
if (!support || !support.autoFormatTriggerCharacters) {
|
||||
return;
|
||||
}
|
||||
|
||||
// register typing listeners that will trigger the format
|
||||
let triggerChars = new CharacterSet();
|
||||
for (let ch of support.autoFormatTriggerCharacters) {
|
||||
triggerChars.add(ch.charCodeAt(0));
|
||||
}
|
||||
this.callOnModel.push(this.editor.onDidType((text: string) => {
|
||||
let lastCharCode = text.charCodeAt(text.length - 1);
|
||||
if (triggerChars.has(lastCharCode)) {
|
||||
this.trigger(String.fromCharCode(lastCharCode));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private trigger(ch: string): void {
|
||||
|
||||
if (this.editor.getSelections().length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
var model = this.editor.getModel(),
|
||||
position = this.editor.getPosition(),
|
||||
canceled = false;
|
||||
|
||||
// install a listener that checks if edits happens before the
|
||||
// position on which we format right now. If so, we won't
|
||||
// apply the format edits
|
||||
var unbind = this.editor.onDidChangeModelContent((e) => {
|
||||
if (e.isFlush) {
|
||||
// a model.setValue() was called
|
||||
// cancel only once
|
||||
canceled = true;
|
||||
unbind.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0, len = e.changes.length; i < len; i++) {
|
||||
const change = e.changes[i];
|
||||
if (change.range.endLineNumber <= position.lineNumber) {
|
||||
// cancel only once
|
||||
canceled = true;
|
||||
unbind.dispose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
let modelOpts = model.getOptions();
|
||||
|
||||
getOnTypeFormattingEdits(model, position, ch, {
|
||||
tabSize: modelOpts.tabSize,
|
||||
insertSpaces: modelOpts.insertSpaces
|
||||
}).then(edits => {
|
||||
return this.workerService.computeMoreMinimalEdits(model.uri, edits, []);
|
||||
}).then(edits => {
|
||||
|
||||
unbind.dispose();
|
||||
|
||||
if (canceled || isFalsyOrEmpty(edits)) {
|
||||
return;
|
||||
}
|
||||
|
||||
EditOperationsCommand.execute(this.editor, edits);
|
||||
alertFormattingEdits(edits);
|
||||
|
||||
}, (err) => {
|
||||
unbind.dispose();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return FormatOnType.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.callOnDispose = dispose(this.callOnDispose);
|
||||
this.callOnModel = dispose(this.callOnModel);
|
||||
}
|
||||
}
|
||||
|
||||
@commonEditorContribution
|
||||
class FormatOnPaste implements editorCommon.IEditorContribution {
|
||||
|
||||
private static ID = 'editor.contrib.formatOnPaste';
|
||||
|
||||
private editor: editorCommon.ICommonCodeEditor;
|
||||
private workerService: IEditorWorkerService;
|
||||
private callOnDispose: IDisposable[];
|
||||
private callOnModel: IDisposable[];
|
||||
|
||||
constructor(editor: editorCommon.ICommonCodeEditor, @IEditorWorkerService workerService: IEditorWorkerService) {
|
||||
this.editor = editor;
|
||||
this.workerService = workerService;
|
||||
this.callOnDispose = [];
|
||||
this.callOnModel = [];
|
||||
|
||||
this.callOnDispose.push(editor.onDidChangeConfiguration(() => this.update()));
|
||||
this.callOnDispose.push(editor.onDidChangeModel(() => this.update()));
|
||||
this.callOnDispose.push(editor.onDidChangeModelLanguage(() => this.update()));
|
||||
this.callOnDispose.push(DocumentRangeFormattingEditProviderRegistry.onDidChange(this.update, this));
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
|
||||
// clean up
|
||||
this.callOnModel = dispose(this.callOnModel);
|
||||
|
||||
// we are disabled
|
||||
if (!this.editor.getConfiguration().contribInfo.formatOnPaste) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no model
|
||||
if (!this.editor.getModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = this.editor.getModel();
|
||||
|
||||
// no support
|
||||
let [support] = DocumentRangeFormattingEditProviderRegistry.ordered(model);
|
||||
if (!support || !support.provideDocumentRangeFormattingEdits) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.callOnModel.push(this.editor.onDidPaste((range: Range) => {
|
||||
this.trigger(range);
|
||||
}));
|
||||
}
|
||||
|
||||
private trigger(range: Range): void {
|
||||
if (this.editor.getSelections().length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this.editor.getModel();
|
||||
const { tabSize, insertSpaces } = model.getOptions();
|
||||
const state = new EditorState(this.editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position);
|
||||
|
||||
getDocumentRangeFormattingEdits(model, range, { tabSize, insertSpaces }).then(edits => {
|
||||
return this.workerService.computeMoreMinimalEdits(model.uri, edits, []);
|
||||
}).then(edits => {
|
||||
if (!state.validate(this.editor) || isFalsyOrEmpty(edits)) {
|
||||
return;
|
||||
}
|
||||
EditOperationsCommand.execute(this.editor, edits);
|
||||
alertFormattingEdits(edits);
|
||||
});
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return FormatOnPaste.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.callOnDispose = dispose(this.callOnDispose);
|
||||
this.callOnModel = dispose(this.callOnModel);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class AbstractFormatAction extends EditorAction {
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): TPromise<void> {
|
||||
|
||||
const workerService = accessor.get(IEditorWorkerService);
|
||||
|
||||
const formattingPromise = this._getFormattingEdits(editor);
|
||||
if (!formattingPromise) {
|
||||
return TPromise.as(void 0);
|
||||
}
|
||||
|
||||
// Capture the state of the editor
|
||||
const state = new EditorState(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position);
|
||||
|
||||
// Receive formatted value from worker
|
||||
return formattingPromise.then(edits => workerService.computeMoreMinimalEdits(editor.getModel().uri, edits, editor.getSelections())).then(edits => {
|
||||
if (!state.validate(editor) || isFalsyOrEmpty(edits)) {
|
||||
return;
|
||||
}
|
||||
|
||||
EditOperationsCommand.execute(editor, edits);
|
||||
alertFormattingEdits(edits);
|
||||
editor.focus();
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract _getFormattingEdits(editor: editorCommon.ICommonCodeEditor): TPromise<editorCommon.ISingleEditOperation[]>;
|
||||
}
|
||||
|
||||
|
||||
@editorAction
|
||||
export class FormatDocumentAction extends AbstractFormatAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.formatDocument',
|
||||
label: nls.localize('formatDocument.label', "Format Document"),
|
||||
alias: 'Format Document',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentFormattingProvider),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F,
|
||||
// secondary: [KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_D)],
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I }
|
||||
},
|
||||
menuOpts: {
|
||||
group: '1_modification',
|
||||
order: 1.3
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _getFormattingEdits(editor: editorCommon.ICommonCodeEditor): TPromise<editorCommon.ISingleEditOperation[]> {
|
||||
const model = editor.getModel();
|
||||
const { tabSize, insertSpaces } = model.getOptions();
|
||||
return getDocumentFormattingEdits(model, { tabSize, insertSpaces });
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class FormatSelectionAction extends AbstractFormatAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.formatSelection',
|
||||
label: nls.localize('formatSelection.label', "Format Selection"),
|
||||
alias: 'Format Code',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider, EditorContextKeys.hasNonEmptySelection),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_F)
|
||||
},
|
||||
menuOpts: {
|
||||
group: '1_modification',
|
||||
order: 1.31
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _getFormattingEdits(editor: editorCommon.ICommonCodeEditor): TPromise<editorCommon.ISingleEditOperation[]> {
|
||||
const model = editor.getModel();
|
||||
const { tabSize, insertSpaces } = model.getOptions();
|
||||
return getDocumentRangeFormattingEdits(model, editor.getSelection(), { tabSize, insertSpaces });
|
||||
}
|
||||
}
|
||||
|
||||
// this is the old format action that does both (format document OR format selection)
|
||||
// and we keep it here such that existing keybinding configurations etc will still work
|
||||
CommandsRegistry.registerCommand('editor.action.format', accessor => {
|
||||
const editor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
|
||||
if (editor) {
|
||||
return new class extends AbstractFormatAction {
|
||||
constructor() {
|
||||
super(<any>{});
|
||||
}
|
||||
_getFormattingEdits(editor: editorCommon.ICommonCodeEditor): TPromise<editorCommon.ISingleEditOperation[]> {
|
||||
const model = editor.getModel();
|
||||
const editorSelection = editor.getSelection();
|
||||
const { tabSize, insertSpaces } = model.getOptions();
|
||||
|
||||
return editorSelection.isEmpty()
|
||||
? getDocumentFormattingEdits(model, { tabSize, insertSpaces })
|
||||
: getDocumentRangeFormattingEdits(model, editorSelection, { tabSize, insertSpaces });
|
||||
}
|
||||
}().run(accessor, editor);
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
107
src/vs/editor/contrib/format/common/format.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { isFalsyOrEmpty } from 'vs/base/common/arrays';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IReadOnlyModel } from 'vs/editor/common/editorCommon';
|
||||
import { CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { DocumentFormattingEditProviderRegistry, DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry, FormattingOptions, TextEdit } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { asWinJsPromise, sequence } from 'vs/base/common/async';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
|
||||
export function getDocumentRangeFormattingEdits(model: IReadOnlyModel, range: Range, options: FormattingOptions): TPromise<TextEdit[]> {
|
||||
|
||||
const providers = DocumentRangeFormattingEditProviderRegistry.ordered(model);
|
||||
|
||||
if (providers.length === 0) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
let result: TextEdit[];
|
||||
return sequence(providers.map(provider => {
|
||||
if (isFalsyOrEmpty(result)) {
|
||||
return () => {
|
||||
return asWinJsPromise(token => provider.provideDocumentRangeFormattingEdits(model, range, options, token)).then(value => {
|
||||
result = value;
|
||||
}, onUnexpectedExternalError);
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})).then(() => result);
|
||||
}
|
||||
|
||||
export function getDocumentFormattingEdits(model: IReadOnlyModel, options: FormattingOptions): TPromise<TextEdit[]> {
|
||||
const providers = DocumentFormattingEditProviderRegistry.ordered(model);
|
||||
|
||||
// try range formatters when no document formatter is registered
|
||||
if (providers.length === 0) {
|
||||
return getDocumentRangeFormattingEdits(model, model.getFullModelRange(), options);
|
||||
}
|
||||
|
||||
let result: TextEdit[];
|
||||
return sequence(providers.map(provider => {
|
||||
if (isFalsyOrEmpty(result)) {
|
||||
return () => {
|
||||
return asWinJsPromise(token => provider.provideDocumentFormattingEdits(model, options, token)).then(value => {
|
||||
result = value;
|
||||
}, onUnexpectedExternalError);
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
})).then(() => result);
|
||||
}
|
||||
|
||||
export function getOnTypeFormattingEdits(model: IReadOnlyModel, position: Position, ch: string, options: FormattingOptions): TPromise<TextEdit[]> {
|
||||
const [support] = OnTypeFormattingEditProviderRegistry.ordered(model);
|
||||
if (!support) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
if (support.autoFormatTriggerCharacters.indexOf(ch) < 0) {
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
return asWinJsPromise((token) => {
|
||||
return support.provideOnTypeFormattingEdits(model, position, ch, options, token);
|
||||
}).then(r => r, onUnexpectedExternalError);
|
||||
}
|
||||
|
||||
CommonEditorRegistry.registerLanguageCommand('_executeFormatRangeProvider', function (accessor, args) {
|
||||
const { resource, range, options } = args;
|
||||
if (!(resource instanceof URI) || !Range.isIRange(range)) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument('resource');
|
||||
}
|
||||
return getDocumentRangeFormattingEdits(model, Range.lift(range), options);
|
||||
});
|
||||
|
||||
CommonEditorRegistry.registerLanguageCommand('_executeFormatDocumentProvider', function (accessor, args) {
|
||||
const { resource, options } = args;
|
||||
if (!(resource instanceof URI)) {
|
||||
throw illegalArgument('resource');
|
||||
}
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument('resource');
|
||||
}
|
||||
|
||||
return getDocumentFormattingEdits(model, options);
|
||||
});
|
||||
|
||||
CommonEditorRegistry.registerDefaultLanguageCommand('_executeFormatOnTypeProvider', function (model, position, args) {
|
||||
const { ch, options } = args;
|
||||
if (typeof ch !== 'string') {
|
||||
throw illegalArgument('ch');
|
||||
}
|
||||
return getOnTypeFormattingEdits(model, position, ch, options);
|
||||
});
|
||||
134
src/vs/editor/contrib/format/common/formatCommand.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { TextEdit } from 'vs/editor/common/modes';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
|
||||
export class EditOperationsCommand implements editorCommon.ICommand {
|
||||
|
||||
static execute(editor: editorCommon.ICommonCodeEditor, edits: TextEdit[]) {
|
||||
const cmd = new EditOperationsCommand(edits, editor.getSelection());
|
||||
if (typeof cmd._newEol === 'number') {
|
||||
editor.getModel().setEOL(cmd._newEol);
|
||||
}
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommand('formatEditsCommand', cmd);
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
|
||||
private _edits: TextEdit[];
|
||||
private _newEol: editorCommon.EndOfLineSequence;
|
||||
|
||||
private _initialSelection: Selection;
|
||||
private _selectionId: string;
|
||||
|
||||
constructor(edits: TextEdit[], initialSelection: Selection) {
|
||||
this._initialSelection = initialSelection;
|
||||
this._edits = [];
|
||||
this._newEol = undefined;
|
||||
|
||||
for (let edit of edits) {
|
||||
if (typeof edit.eol === 'number') {
|
||||
this._newEol = edit.eol;
|
||||
}
|
||||
if (edit.range && typeof edit.text === 'string') {
|
||||
this._edits.push(edit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getEditOperations(model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder): void {
|
||||
|
||||
for (let edit of this._edits) {
|
||||
// We know that this edit.range comes from the mirror model, so it should only contain \n and no \r's
|
||||
let trimEdit = EditOperationsCommand.trimEdit(edit, model);
|
||||
if (trimEdit !== null) { // produced above in case the edit.text is identical to the existing text
|
||||
builder.addEditOperation(Range.lift(edit.range), edit.text);
|
||||
}
|
||||
}
|
||||
|
||||
var selectionIsSet = false;
|
||||
if (Array.isArray(this._edits) && this._edits.length === 1 && this._initialSelection.isEmpty()) {
|
||||
if (this._edits[0].range.startColumn === this._initialSelection.endColumn &&
|
||||
this._edits[0].range.startLineNumber === this._initialSelection.endLineNumber) {
|
||||
selectionIsSet = true;
|
||||
this._selectionId = builder.trackSelection(this._initialSelection, true);
|
||||
} else if (this._edits[0].range.endColumn === this._initialSelection.startColumn &&
|
||||
this._edits[0].range.endLineNumber === this._initialSelection.startLineNumber) {
|
||||
selectionIsSet = true;
|
||||
this._selectionId = builder.trackSelection(this._initialSelection, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectionIsSet) {
|
||||
this._selectionId = builder.trackSelection(this._initialSelection);
|
||||
}
|
||||
}
|
||||
|
||||
public computeCursorState(model: editorCommon.ITokenizedModel, helper: editorCommon.ICursorStateComputerData): Selection {
|
||||
return helper.getTrackedSelection(this._selectionId);
|
||||
}
|
||||
|
||||
static fixLineTerminators(edit: editorCommon.ISingleEditOperation, model: editorCommon.ITokenizedModel): void {
|
||||
edit.text = edit.text.replace(/\r\n|\r|\n/g, model.getEOL());
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to minimize the edits by removing changes that appear on the edges of the range which are identical
|
||||
* to the current text.
|
||||
*
|
||||
* The reason this was introduced is to allow better selection tracking of the current cursor and solve
|
||||
* bug #15108. There the cursor was jumping since the tracked selection was in the middle of the range edit
|
||||
* and was lost.
|
||||
*/
|
||||
static trimEdit(edit: editorCommon.ISingleEditOperation, model: editorCommon.ITokenizedModel): editorCommon.ISingleEditOperation {
|
||||
|
||||
this.fixLineTerminators(edit, model);
|
||||
|
||||
return this._trimEdit(model.validateRange(edit.range), edit.text, edit.forceMoveMarkers, model);
|
||||
}
|
||||
|
||||
static _trimEdit(editRange: Range, editText: string, editForceMoveMarkers: boolean, model: editorCommon.ITokenizedModel): editorCommon.ISingleEditOperation {
|
||||
|
||||
let currentText = model.getValueInRange(editRange);
|
||||
|
||||
// Find the equal characters in the front
|
||||
let commonPrefixLength = strings.commonPrefixLength(editText, currentText);
|
||||
|
||||
// If the two strings are identical, return no edit (no-op)
|
||||
if (commonPrefixLength === currentText.length && commonPrefixLength === editText.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (commonPrefixLength > 0) {
|
||||
// Apply front trimming
|
||||
let newStartPosition = model.modifyPosition(editRange.getStartPosition(), commonPrefixLength);
|
||||
editRange = new Range(newStartPosition.lineNumber, newStartPosition.column, editRange.endLineNumber, editRange.endColumn);
|
||||
editText = editText.substring(commonPrefixLength);
|
||||
currentText = currentText.substr(commonPrefixLength);
|
||||
}
|
||||
|
||||
// Find the equal characters in the rear
|
||||
let commonSuffixLength = strings.commonSuffixLength(editText, currentText);
|
||||
|
||||
if (commonSuffixLength > 0) {
|
||||
// Apply rear trimming
|
||||
let newEndPosition = model.modifyPosition(editRange.getEndPosition(), -commonSuffixLength);
|
||||
editRange = new Range(editRange.startLineNumber, editRange.startColumn, newEndPosition.lineNumber, newEndPosition.column);
|
||||
editText = editText.substring(0, editText.length - commonSuffixLength);
|
||||
currentText = currentText.substring(0, currentText.length - commonSuffixLength);
|
||||
}
|
||||
|
||||
return {
|
||||
text: editText,
|
||||
range: editRange,
|
||||
forceMoveMarkers: editForceMoveMarkers
|
||||
};
|
||||
}
|
||||
}
|
||||
313
src/vs/editor/contrib/format/test/common/formatCommand.test.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ISingleEditOperation } from 'vs/editor/common/editorCommon';
|
||||
import { Model } from 'vs/editor/common/model/model';
|
||||
import { EditOperationsCommand } from 'vs/editor/contrib/format/common/formatCommand';
|
||||
import { testCommand } from 'vs/editor/test/common/commands/commandTestUtils';
|
||||
|
||||
function editOp(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number, text: string[]): ISingleEditOperation {
|
||||
return {
|
||||
range: new Range(startLineNumber, startColumn, endLineNumber, endColumn),
|
||||
text: text.join('\n'),
|
||||
forceMoveMarkers: false
|
||||
};
|
||||
}
|
||||
|
||||
suite('FormatCommand.trimEdit', () => {
|
||||
function testTrimEdit(lines: string[], edit: ISingleEditOperation, expected: ISingleEditOperation): void {
|
||||
let model = Model.createFromString(lines.join('\n'));
|
||||
let actual = EditOperationsCommand.trimEdit(edit, model);
|
||||
assert.deepEqual(actual, expected);
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
test('single-line no-op', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
editOp(1, 1, 1, 10, [
|
||||
'some text'
|
||||
]),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test('multi-line no-op 1', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
editOp(1, 1, 2, 16, [
|
||||
'some text',
|
||||
'some other text'
|
||||
]),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test('multi-line no-op 2', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
editOp(1, 1, 2, 1, [
|
||||
'some text',
|
||||
''
|
||||
]),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test('simple prefix, no suffix', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
editOp(1, 1, 1, 10, [
|
||||
'some interesting thing'
|
||||
]),
|
||||
editOp(1, 6, 1, 10, [
|
||||
'interesting thing'
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('whole line prefix, no suffix', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
editOp(1, 1, 1, 10, [
|
||||
'some text',
|
||||
'interesting thing'
|
||||
]),
|
||||
editOp(1, 10, 1, 10, [
|
||||
'',
|
||||
'interesting thing'
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('multi-line prefix, no suffix', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
editOp(1, 1, 2, 16, [
|
||||
'some text',
|
||||
'some other interesting thing'
|
||||
]),
|
||||
editOp(2, 12, 2, 16, [
|
||||
'interesting thing'
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('no prefix, simple suffix', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
editOp(1, 1, 1, 10, [
|
||||
'interesting text'
|
||||
]),
|
||||
editOp(1, 1, 1, 5, [
|
||||
'interesting'
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('no prefix, whole line suffix', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
editOp(1, 1, 1, 10, [
|
||||
'interesting thing',
|
||||
'some text'
|
||||
]),
|
||||
editOp(1, 1, 1, 1, [
|
||||
'interesting thing',
|
||||
''
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('no prefix, multi-line suffix', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
editOp(1, 1, 2, 16, [
|
||||
'interesting thing text',
|
||||
'some other text'
|
||||
]),
|
||||
editOp(1, 1, 1, 5, [
|
||||
'interesting thing'
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('no overlapping prefix & suffix', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some cool text'
|
||||
],
|
||||
editOp(1, 1, 1, 15, [
|
||||
'some interesting text'
|
||||
]),
|
||||
editOp(1, 6, 1, 10, [
|
||||
'interesting'
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('overlapping prefix & suffix 1', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some cool text'
|
||||
],
|
||||
editOp(1, 1, 1, 15, [
|
||||
'some cool cool text'
|
||||
]),
|
||||
editOp(1, 11, 1, 11, [
|
||||
'cool '
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
test('overlapping prefix & suffix 2', () => {
|
||||
testTrimEdit(
|
||||
[
|
||||
'some cool cool text'
|
||||
],
|
||||
editOp(1, 1, 1, 29, [
|
||||
'some cool text'
|
||||
]),
|
||||
editOp(1, 11, 1, 16, [
|
||||
''
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('FormatCommand', () => {
|
||||
function testFormatCommand(lines: string[], selection: Selection, edits: ISingleEditOperation[], expectedLines: string[], expectedSelection: Selection): void {
|
||||
testCommand(lines, null, selection, (sel) => new EditOperationsCommand(edits, sel), expectedLines, expectedSelection);
|
||||
}
|
||||
|
||||
test('no-op', () => {
|
||||
testFormatCommand(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
new Selection(2, 1, 2, 5),
|
||||
[
|
||||
editOp(1, 1, 2, 16, [
|
||||
'some text',
|
||||
'some other text'
|
||||
])
|
||||
],
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
new Selection(2, 1, 2, 5)
|
||||
);
|
||||
});
|
||||
|
||||
test('trim beginning', () => {
|
||||
testFormatCommand(
|
||||
[
|
||||
'some text',
|
||||
'some other text'
|
||||
],
|
||||
new Selection(2, 1, 2, 5),
|
||||
[
|
||||
editOp(1, 1, 2, 16, [
|
||||
'some text',
|
||||
'some new other text'
|
||||
])
|
||||
],
|
||||
[
|
||||
'some text',
|
||||
'some new other text'
|
||||
],
|
||||
new Selection(2, 1, 2, 5)
|
||||
);
|
||||
});
|
||||
|
||||
test('issue #144', () => {
|
||||
testFormatCommand(
|
||||
[
|
||||
'package caddy',
|
||||
'',
|
||||
'func main() {',
|
||||
'\tfmt.Println("Hello World! :)")',
|
||||
'}',
|
||||
''
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
editOp(1, 1, 6, 1, [
|
||||
'package caddy',
|
||||
'',
|
||||
'import "fmt"',
|
||||
'',
|
||||
'func main() {',
|
||||
'\tfmt.Println("Hello World! :)")',
|
||||
'}',
|
||||
''
|
||||
])
|
||||
],
|
||||
[
|
||||
'package caddy',
|
||||
'',
|
||||
'import "fmt"',
|
||||
'',
|
||||
'func main() {',
|
||||
'\tfmt.Println("Hello World! :)")',
|
||||
'}',
|
||||
''
|
||||
],
|
||||
new Selection(1, 1, 1, 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('issue #23765', () => {
|
||||
testFormatCommand(
|
||||
[
|
||||
' let a;'
|
||||
],
|
||||
new Selection(1, 1, 1, 1),
|
||||
[
|
||||
editOp(1, 1, 1, 2, [
|
||||
''
|
||||
])
|
||||
],
|
||||
[
|
||||
'let a;'
|
||||
],
|
||||
new Selection(1, 1, 1, 1)
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,205 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./goToDeclarationMouse';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { ICodeEditor, IEditorMouseEvent, IMouseTarget } from 'vs/editor/browser/editorBrowser';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
import Event, { Emitter } from 'vs/base/common/event';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
|
||||
function hasModifier(e: { ctrlKey: boolean; shiftKey: boolean; altKey: boolean; metaKey: boolean }, modifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'): boolean {
|
||||
return !!e[modifier];
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that encapsulates the various trigger modifiers logic needed for go to definition.
|
||||
*/
|
||||
export class ClickLinkMouseEvent {
|
||||
|
||||
public readonly target: IMouseTarget;
|
||||
public readonly hasTriggerModifier: boolean;
|
||||
public readonly hasSideBySideModifier: boolean;
|
||||
public readonly isNoneOrSingleMouseDown: boolean;
|
||||
|
||||
constructor(source: IEditorMouseEvent, opts: ClickLinkOptions) {
|
||||
this.target = source.target;
|
||||
this.hasTriggerModifier = hasModifier(source.event, opts.triggerModifier);
|
||||
this.hasSideBySideModifier = hasModifier(source.event, opts.triggerSideBySideModifier);
|
||||
this.isNoneOrSingleMouseDown = (browser.isIE || source.event.detail <= 1); // IE does not support event.detail properly
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An event that encapsulates the various trigger modifiers logic needed for go to definition.
|
||||
*/
|
||||
export class ClickLinkKeyboardEvent {
|
||||
|
||||
public readonly keyCodeIsTriggerKey: boolean;
|
||||
public readonly keyCodeIsSideBySideKey: boolean;
|
||||
public readonly hasTriggerModifier: boolean;
|
||||
|
||||
constructor(source: IKeyboardEvent, opts: ClickLinkOptions) {
|
||||
this.keyCodeIsTriggerKey = (source.keyCode === opts.triggerKey);
|
||||
this.keyCodeIsSideBySideKey = (source.keyCode === opts.triggerSideBySideKey);
|
||||
this.hasTriggerModifier = hasModifier(source, opts.triggerModifier);
|
||||
}
|
||||
}
|
||||
|
||||
export class ClickLinkOptions {
|
||||
|
||||
public readonly triggerKey: KeyCode;
|
||||
public readonly triggerModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey';
|
||||
public readonly triggerSideBySideKey: KeyCode;
|
||||
public readonly triggerSideBySideModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey';
|
||||
|
||||
constructor(
|
||||
triggerKey: KeyCode,
|
||||
triggerModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey',
|
||||
triggerSideBySideKey: KeyCode,
|
||||
triggerSideBySideModifier: 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey'
|
||||
) {
|
||||
this.triggerKey = triggerKey;
|
||||
this.triggerModifier = triggerModifier;
|
||||
this.triggerSideBySideKey = triggerSideBySideKey;
|
||||
this.triggerSideBySideModifier = triggerSideBySideModifier;
|
||||
}
|
||||
|
||||
public equals(other: ClickLinkOptions): boolean {
|
||||
return (
|
||||
this.triggerKey === other.triggerKey
|
||||
&& this.triggerModifier === other.triggerModifier
|
||||
&& this.triggerSideBySideKey === other.triggerSideBySideKey
|
||||
&& this.triggerSideBySideModifier === other.triggerSideBySideModifier
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function createOptions(multiCursorModifier: 'altKey' | 'ctrlKey' | 'metaKey'): ClickLinkOptions {
|
||||
if (multiCursorModifier === 'altKey') {
|
||||
if (platform.isMacintosh) {
|
||||
return new ClickLinkOptions(KeyCode.Meta, 'metaKey', KeyCode.Alt, 'altKey');
|
||||
}
|
||||
return new ClickLinkOptions(KeyCode.Ctrl, 'ctrlKey', KeyCode.Alt, 'altKey');
|
||||
}
|
||||
|
||||
if (platform.isMacintosh) {
|
||||
return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Meta, 'metaKey');
|
||||
}
|
||||
return new ClickLinkOptions(KeyCode.Alt, 'altKey', KeyCode.Ctrl, 'ctrlKey');
|
||||
}
|
||||
|
||||
export class ClickLinkGesture extends Disposable {
|
||||
|
||||
private readonly _onMouseMoveOrRelevantKeyDown: Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent]> = this._register(new Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent]>());
|
||||
public readonly onMouseMoveOrRelevantKeyDown: Event<[ClickLinkMouseEvent, ClickLinkKeyboardEvent]> = this._onMouseMoveOrRelevantKeyDown.event;
|
||||
|
||||
private readonly _onExecute: Emitter<ClickLinkMouseEvent> = this._register(new Emitter<ClickLinkMouseEvent>());
|
||||
public readonly onExecute: Event<ClickLinkMouseEvent> = this._onExecute.event;
|
||||
|
||||
private readonly _onCancel: Emitter<void> = this._register(new Emitter<void>());
|
||||
public readonly onCancel: Event<void> = this._onCancel.event;
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
private _opts: ClickLinkOptions;
|
||||
|
||||
private lastMouseMoveEvent: ClickLinkMouseEvent;
|
||||
private hasTriggerKeyOnMouseDown: boolean;
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
super();
|
||||
|
||||
this._editor = editor;
|
||||
this._opts = createOptions(this._editor.getConfiguration().multiCursorModifier);
|
||||
|
||||
this.lastMouseMoveEvent = null;
|
||||
this.hasTriggerKeyOnMouseDown = false;
|
||||
|
||||
this._register(this._editor.onDidChangeConfiguration((e) => {
|
||||
if (e.multiCursorModifier) {
|
||||
const newOpts = createOptions(this._editor.getConfiguration().multiCursorModifier);
|
||||
if (this._opts.equals(newOpts)) {
|
||||
return;
|
||||
}
|
||||
this._opts = newOpts;
|
||||
this.lastMouseMoveEvent = null;
|
||||
this.hasTriggerKeyOnMouseDown = false;
|
||||
this._onCancel.fire();
|
||||
}
|
||||
}));
|
||||
this._register(this._editor.onMouseMove((e: IEditorMouseEvent) => this.onEditorMouseMove(new ClickLinkMouseEvent(e, this._opts))));
|
||||
this._register(this._editor.onMouseDown((e: IEditorMouseEvent) => this.onEditorMouseDown(new ClickLinkMouseEvent(e, this._opts))));
|
||||
this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this.onEditorMouseUp(new ClickLinkMouseEvent(e, this._opts))));
|
||||
this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this.onEditorKeyDown(new ClickLinkKeyboardEvent(e, this._opts))));
|
||||
this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(new ClickLinkKeyboardEvent(e, this._opts))));
|
||||
this._register(this._editor.onMouseDrag(() => this.resetHandler()));
|
||||
|
||||
this._register(this._editor.onDidChangeCursorSelection((e) => this.onDidChangeCursorSelection(e)));
|
||||
this._register(this._editor.onDidChangeModel((e) => this.resetHandler()));
|
||||
this._register(this._editor.onDidChangeModelContent(() => this.resetHandler()));
|
||||
this._register(this._editor.onDidScrollChange((e) => {
|
||||
if (e.scrollTopChanged || e.scrollLeftChanged) {
|
||||
this.resetHandler();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private onDidChangeCursorSelection(e: ICursorSelectionChangedEvent): void {
|
||||
if (e.selection && e.selection.startColumn !== e.selection.endColumn) {
|
||||
this.resetHandler(); // immediately stop this feature if the user starts to select (https://github.com/Microsoft/vscode/issues/7827)
|
||||
}
|
||||
}
|
||||
|
||||
private onEditorMouseMove(mouseEvent: ClickLinkMouseEvent): void {
|
||||
this.lastMouseMoveEvent = mouseEvent;
|
||||
|
||||
this._onMouseMoveOrRelevantKeyDown.fire([mouseEvent, null]);
|
||||
}
|
||||
|
||||
private onEditorMouseDown(mouseEvent: ClickLinkMouseEvent): void {
|
||||
// We need to record if we had the trigger key on mouse down because someone might select something in the editor
|
||||
// holding the mouse down and then while mouse is down start to press Ctrl/Cmd to start a copy operation and then
|
||||
// release the mouse button without wanting to do the navigation.
|
||||
// With this flag we prevent goto definition if the mouse was down before the trigger key was pressed.
|
||||
this.hasTriggerKeyOnMouseDown = mouseEvent.hasTriggerModifier;
|
||||
}
|
||||
|
||||
private onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void {
|
||||
if (this.hasTriggerKeyOnMouseDown) {
|
||||
this._onExecute.fire(mouseEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private onEditorKeyDown(e: ClickLinkKeyboardEvent): void {
|
||||
if (
|
||||
this.lastMouseMoveEvent
|
||||
&& (
|
||||
e.keyCodeIsTriggerKey // User just pressed Ctrl/Cmd (normal goto definition)
|
||||
|| (e.keyCodeIsSideBySideKey && e.hasTriggerModifier) // User pressed Ctrl/Cmd+Alt (goto definition to the side)
|
||||
)
|
||||
) {
|
||||
this._onMouseMoveOrRelevantKeyDown.fire([this.lastMouseMoveEvent, e]);
|
||||
} else if (e.hasTriggerModifier) {
|
||||
this._onCancel.fire(); // remove decorations if user holds another key with ctrl/cmd to prevent accident goto declaration
|
||||
}
|
||||
}
|
||||
|
||||
private onEditorKeyUp(e: ClickLinkKeyboardEvent): void {
|
||||
if (e.keyCodeIsTriggerKey) {
|
||||
this._onCancel.fire();
|
||||
}
|
||||
}
|
||||
|
||||
private resetHandler(): void {
|
||||
this.lastMouseMoveEvent = null;
|
||||
this.hasTriggerKeyOnMouseDown = false;
|
||||
this._onCancel.fire();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IReadOnlyModel } from 'vs/editor/common/editorCommon';
|
||||
import { CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions';
|
||||
import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry';
|
||||
import { DefinitionProviderRegistry, ImplementationProviderRegistry, TypeDefinitionProviderRegistry, Location } from 'vs/editor/common/modes';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { asWinJsPromise } from 'vs/base/common/async';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
|
||||
function outputResults(promises: TPromise<Location | Location[]>[]) {
|
||||
return TPromise.join(promises).then(allReferences => {
|
||||
let result: Location[] = [];
|
||||
for (let references of allReferences) {
|
||||
if (Array.isArray(references)) {
|
||||
result.push(...references);
|
||||
} else if (references) {
|
||||
result.push(references);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
function getDefinitions<T>(
|
||||
model: IReadOnlyModel,
|
||||
position: Position,
|
||||
registry: LanguageFeatureRegistry<T>,
|
||||
provide: (provider: T, model: IReadOnlyModel, position: Position, token: CancellationToken) => Location | Location[] | Thenable<Location | Location[]>
|
||||
): TPromise<Location[]> {
|
||||
const provider = registry.ordered(model);
|
||||
|
||||
// get results
|
||||
const promises = provider.map((provider, idx) => {
|
||||
return asWinJsPromise((token) => {
|
||||
return provide(provider, model, position, token);
|
||||
}).then(undefined, err => {
|
||||
onUnexpectedExternalError(err);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
return outputResults(promises);
|
||||
}
|
||||
|
||||
|
||||
export function getDefinitionsAtPosition(model: IReadOnlyModel, position: Position): TPromise<Location[]> {
|
||||
return getDefinitions(model, position, DefinitionProviderRegistry, (provider, model, position, token) => {
|
||||
return provider.provideDefinition(model, position, token);
|
||||
});
|
||||
}
|
||||
|
||||
export function getImplementationsAtPosition(model: IReadOnlyModel, position: Position): TPromise<Location[]> {
|
||||
return getDefinitions(model, position, ImplementationProviderRegistry, (provider, model, position, token) => {
|
||||
return provider.provideImplementation(model, position, token);
|
||||
});
|
||||
}
|
||||
|
||||
export function getTypeDefinitionsAtPosition(model: IReadOnlyModel, position: Position): TPromise<Location[]> {
|
||||
return getDefinitions(model, position, TypeDefinitionProviderRegistry, (provider, model, position, token) => {
|
||||
return provider.provideTypeDefinition(model, position, token);
|
||||
});
|
||||
}
|
||||
|
||||
CommonEditorRegistry.registerDefaultLanguageCommand('_executeDefinitionProvider', getDefinitionsAtPosition);
|
||||
CommonEditorRegistry.registerDefaultLanguageCommand('_executeImplementationProvider', getImplementationsAtPosition);
|
||||
CommonEditorRegistry.registerDefaultLanguageCommand('_executeTypeDefinitionProvider', getTypeDefinitionsAtPosition);
|
||||
@@ -0,0 +1,371 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IEditorService } from 'vs/platform/editor/common/editor';
|
||||
import { IMessageService } from 'vs/platform/message/common/message';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { editorAction, IActionOptions, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { Location } from 'vs/editor/common/modes';
|
||||
import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition } from './goToDeclaration';
|
||||
import { ReferencesController } from 'vs/editor/contrib/referenceSearch/browser/referencesController';
|
||||
import { ReferencesModel } from 'vs/editor/contrib/referenceSearch/browser/referencesModel';
|
||||
import { PeekContext } from 'vs/editor/contrib/zoneWidget/browser/peekViewWidget';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { MessageController } from './messageController';
|
||||
import * as corePosition from 'vs/editor/common/core/position';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { IProgressService } from 'vs/platform/progress/common/progress';
|
||||
|
||||
export class DefinitionActionConfig {
|
||||
|
||||
constructor(
|
||||
public readonly openToSide = false,
|
||||
public readonly openInPeek = false,
|
||||
public readonly filterCurrent = true,
|
||||
public readonly showMessage = true,
|
||||
) {
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
export class DefinitionAction extends EditorAction {
|
||||
|
||||
private _configuration: DefinitionActionConfig;
|
||||
|
||||
constructor(configuration: DefinitionActionConfig, opts: IActionOptions) {
|
||||
super(opts);
|
||||
this._configuration = configuration;
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): TPromise<void> {
|
||||
const messageService = accessor.get(IMessageService);
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const progressService = accessor.get(IProgressService);
|
||||
|
||||
const model = editor.getModel();
|
||||
const pos = editor.getPosition();
|
||||
|
||||
const definitionPromise = this._getDeclarationsAtPosition(model, pos).then(references => {
|
||||
|
||||
if (model.isDisposed() || editor.getModel() !== model) {
|
||||
// new model, no more model
|
||||
return;
|
||||
}
|
||||
|
||||
// * remove falsy references
|
||||
// * find reference at the current pos
|
||||
let idxOfCurrent = -1;
|
||||
let result: Location[] = [];
|
||||
for (let i = 0; i < references.length; i++) {
|
||||
let reference = references[i];
|
||||
if (!reference || !reference.range) {
|
||||
continue;
|
||||
}
|
||||
let { uri, range } = reference;
|
||||
let newLen = result.push({
|
||||
uri,
|
||||
range
|
||||
});
|
||||
if (this._configuration.filterCurrent
|
||||
&& uri.toString() === model.uri.toString()
|
||||
&& Range.containsPosition(range, pos)
|
||||
&& idxOfCurrent === -1
|
||||
) {
|
||||
idxOfCurrent = newLen - 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length === 0) {
|
||||
// no result -> show message
|
||||
if (this._configuration.showMessage) {
|
||||
const info = model.getWordAtPosition(pos);
|
||||
MessageController.get(editor).showMessage(this._getNoResultFoundMessage(info), pos);
|
||||
}
|
||||
} else if (result.length === 1 && idxOfCurrent !== -1) {
|
||||
// only the position at which we are -> adjust selection
|
||||
let [current] = result;
|
||||
this._openReference(editorService, current, false);
|
||||
|
||||
} else {
|
||||
// handle multile results
|
||||
this._onResult(editorService, editor, new ReferencesModel(result));
|
||||
}
|
||||
|
||||
}, (err) => {
|
||||
// report an error
|
||||
messageService.show(Severity.Error, err);
|
||||
});
|
||||
|
||||
progressService.showWhile(definitionPromise, 250);
|
||||
return definitionPromise;
|
||||
}
|
||||
|
||||
protected _getDeclarationsAtPosition(model: editorCommon.IModel, position: corePosition.Position): TPromise<Location[]> {
|
||||
return getDefinitionsAtPosition(model, position);
|
||||
}
|
||||
|
||||
protected _getNoResultFoundMessage(info?: editorCommon.IWordAtPosition): string {
|
||||
return info && info.word
|
||||
? nls.localize('noResultWord', "No definition found for '{0}'", info.word)
|
||||
: nls.localize('generic.noResults', "No definition found");
|
||||
}
|
||||
|
||||
protected _getMetaTitle(model: ReferencesModel): string {
|
||||
return model.references.length > 1 && nls.localize('meta.title', " – {0} definitions", model.references.length);
|
||||
}
|
||||
|
||||
private _onResult(editorService: IEditorService, editor: editorCommon.ICommonCodeEditor, model: ReferencesModel) {
|
||||
|
||||
const msg = model.getAriaMessage();
|
||||
alert(msg);
|
||||
|
||||
if (this._configuration.openInPeek) {
|
||||
this._openInPeek(editorService, editor, model);
|
||||
} else {
|
||||
let next = model.nearestReference(editor.getModel().uri, editor.getPosition());
|
||||
this._openReference(editorService, next, this._configuration.openToSide).then(editor => {
|
||||
if (editor && model.references.length > 1) {
|
||||
this._openInPeek(editorService, editor, model);
|
||||
} else {
|
||||
model.dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _openReference(editorService: IEditorService, reference: Location, sideBySide: boolean): TPromise<editorCommon.ICommonCodeEditor> {
|
||||
let { uri, range } = reference;
|
||||
return editorService.openEditor({
|
||||
resource: uri,
|
||||
options: {
|
||||
selection: Range.collapseToStart(range),
|
||||
revealIfVisible: !sideBySide
|
||||
}
|
||||
}, sideBySide).then(editor => {
|
||||
return editor && <editorCommon.ICommonCodeEditor>editor.getControl();
|
||||
});
|
||||
}
|
||||
|
||||
private _openInPeek(editorService: IEditorService, target: editorCommon.ICommonCodeEditor, model: ReferencesModel) {
|
||||
let controller = ReferencesController.get(target);
|
||||
if (controller) {
|
||||
controller.toggleWidget(target.getSelection(), TPromise.as(model), {
|
||||
getMetaTitle: (model) => {
|
||||
return this._getMetaTitle(model);
|
||||
},
|
||||
onGoto: (reference) => {
|
||||
controller.closeWidget();
|
||||
return this._openReference(editorService, reference, false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
model.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const goToDeclarationKb = platform.isWeb
|
||||
? KeyMod.CtrlCmd | KeyCode.F12
|
||||
: KeyCode.F12;
|
||||
|
||||
@editorAction
|
||||
export class GoToDefinitionAction extends DefinitionAction {
|
||||
|
||||
public static ID = 'editor.action.goToDeclaration';
|
||||
|
||||
constructor() {
|
||||
super(new DefinitionActionConfig(), {
|
||||
id: GoToDefinitionAction.ID,
|
||||
label: nls.localize('actions.goToDecl.label', "Go to Definition"),
|
||||
alias: 'Go to Definition',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasDefinitionProvider,
|
||||
EditorContextKeys.isInEmbeddedEditor.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: goToDeclarationKb
|
||||
},
|
||||
menuOpts: {
|
||||
group: 'navigation',
|
||||
order: 1.1
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class OpenDefinitionToSideAction extends DefinitionAction {
|
||||
|
||||
public static ID = 'editor.action.openDeclarationToTheSide';
|
||||
|
||||
constructor() {
|
||||
super(new DefinitionActionConfig(true), {
|
||||
id: OpenDefinitionToSideAction.ID,
|
||||
label: nls.localize('actions.goToDeclToSide.label', "Open Definition to the Side"),
|
||||
alias: 'Open Definition to the Side',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasDefinitionProvider,
|
||||
EditorContextKeys.isInEmbeddedEditor.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, goToDeclarationKb)
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class PeekDefinitionAction extends DefinitionAction {
|
||||
constructor() {
|
||||
super(new DefinitionActionConfig(void 0, true, false), {
|
||||
id: 'editor.action.previewDeclaration',
|
||||
label: nls.localize('actions.previewDecl.label', "Peek Definition"),
|
||||
alias: 'Peek Definition',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasDefinitionProvider,
|
||||
PeekContext.notInPeekEditor,
|
||||
EditorContextKeys.isInEmbeddedEditor.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.Alt | KeyCode.F12,
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F10 }
|
||||
},
|
||||
menuOpts: {
|
||||
group: 'navigation',
|
||||
order: 1.2
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ImplementationAction extends DefinitionAction {
|
||||
protected _getDeclarationsAtPosition(model: editorCommon.IModel, position: corePosition.Position): TPromise<Location[]> {
|
||||
return getImplementationsAtPosition(model, position);
|
||||
}
|
||||
|
||||
protected _getNoResultFoundMessage(info?: editorCommon.IWordAtPosition): string {
|
||||
return info && info.word
|
||||
? nls.localize('goToImplementation.noResultWord', "No implementation found for '{0}'", info.word)
|
||||
: nls.localize('goToImplementation.generic.noResults', "No implementation found");
|
||||
}
|
||||
|
||||
protected _getMetaTitle(model: ReferencesModel): string {
|
||||
return model.references.length > 1 && nls.localize('meta.implementations.title', " – {0} implementations", model.references.length);
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class GoToImplementationAction extends ImplementationAction {
|
||||
|
||||
public static ID = 'editor.action.goToImplementation';
|
||||
|
||||
constructor() {
|
||||
super(new DefinitionActionConfig(), {
|
||||
id: GoToImplementationAction.ID,
|
||||
label: nls.localize('actions.goToImplementation.label', "Go to Implementation"),
|
||||
alias: 'Go to Implementation',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasImplementationProvider,
|
||||
EditorContextKeys.isInEmbeddedEditor.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.F12
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class PeekImplementationAction extends ImplementationAction {
|
||||
|
||||
public static ID = 'editor.action.peekImplementation';
|
||||
|
||||
constructor() {
|
||||
super(new DefinitionActionConfig(false, true, false), {
|
||||
id: PeekImplementationAction.ID,
|
||||
label: nls.localize('actions.peekImplementation.label', "Peek Implementation"),
|
||||
alias: 'Peek Implementation',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasImplementationProvider,
|
||||
EditorContextKeys.isInEmbeddedEditor.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F12
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class TypeDefinitionAction extends DefinitionAction {
|
||||
protected _getDeclarationsAtPosition(model: editorCommon.IModel, position: corePosition.Position): TPromise<Location[]> {
|
||||
return getTypeDefinitionsAtPosition(model, position);
|
||||
}
|
||||
|
||||
protected _getNoResultFoundMessage(info?: editorCommon.IWordAtPosition): string {
|
||||
return info && info.word
|
||||
? nls.localize('goToTypeDefinition.noResultWord', "No type definition found for '{0}'", info.word)
|
||||
: nls.localize('goToTypeDefinition.generic.noResults', "No type definition found");
|
||||
}
|
||||
|
||||
protected _getMetaTitle(model: ReferencesModel): string {
|
||||
return model.references.length > 1 && nls.localize('meta.typeDefinitions.title', " – {0} type definitions", model.references.length);
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class GoToTypeDefintionAction extends TypeDefinitionAction {
|
||||
|
||||
public static ID = 'editor.action.goToTypeDefinition';
|
||||
|
||||
constructor() {
|
||||
super(new DefinitionActionConfig(), {
|
||||
id: GoToTypeDefintionAction.ID,
|
||||
label: nls.localize('actions.goToTypeDefinition.label', "Go to Type Definition"),
|
||||
alias: 'Go to Type Definition',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasTypeDefinitionProvider,
|
||||
EditorContextKeys.isInEmbeddedEditor.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: 0
|
||||
},
|
||||
menuOpts: {
|
||||
group: 'navigation',
|
||||
order: 1.4
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class PeekTypeDefinitionAction extends TypeDefinitionAction {
|
||||
|
||||
public static ID = 'editor.action.peekTypeDefinition';
|
||||
|
||||
constructor() {
|
||||
super(new DefinitionActionConfig(false, true, false), {
|
||||
id: PeekTypeDefinitionAction.ID,
|
||||
label: nls.localize('actions.peekTypeDefinition.label', "Peek Type Definition"),
|
||||
alias: 'Peek Type Definition',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasTypeDefinitionProvider,
|
||||
EditorContextKeys.isInEmbeddedEditor.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: 0
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .goto-definition-link {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./goToDeclarationMouse';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Throttler } from 'vs/base/common/async';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Location, DefinitionProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { ICodeEditor, IMouseTarget, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
import { getDefinitionsAtPosition } from './goToDeclaration';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { EditorState, CodeEditorStateFlag } from 'vs/editor/common/core/editorState';
|
||||
import { DefinitionAction, DefinitionActionConfig } from './goToDeclarationCommands';
|
||||
import { ClickLinkGesture, ClickLinkMouseEvent, ClickLinkKeyboardEvent } from 'vs/editor/contrib/goToDeclaration/browser/clickLinkGesture';
|
||||
|
||||
@editorContribution
|
||||
class GotoDefinitionWithMouseEditorContribution implements editorCommon.IEditorContribution {
|
||||
|
||||
private static ID = 'editor.contrib.gotodefinitionwithmouse';
|
||||
static MAX_SOURCE_PREVIEW_LINES = 8;
|
||||
|
||||
private editor: ICodeEditor;
|
||||
private toUnhook: IDisposable[];
|
||||
private decorations: string[];
|
||||
private currentWordUnderMouse: editorCommon.IWordAtPosition;
|
||||
private throttler: Throttler;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@ITextModelService private textModelResolverService: ITextModelService,
|
||||
@IModeService private modeService: IModeService
|
||||
) {
|
||||
this.toUnhook = [];
|
||||
this.decorations = [];
|
||||
this.editor = editor;
|
||||
this.throttler = new Throttler();
|
||||
|
||||
let linkGesture = new ClickLinkGesture(editor);
|
||||
this.toUnhook.push(linkGesture);
|
||||
|
||||
this.toUnhook.push(linkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
|
||||
this.startFindDefinition(mouseEvent, keyboardEvent);
|
||||
}));
|
||||
|
||||
this.toUnhook.push(linkGesture.onExecute((mouseEvent: ClickLinkMouseEvent) => {
|
||||
if (this.isEnabled(mouseEvent)) {
|
||||
this.gotoDefinition(mouseEvent.target, mouseEvent.hasSideBySideModifier).done(() => {
|
||||
this.removeDecorations();
|
||||
}, (error: Error) => {
|
||||
this.removeDecorations();
|
||||
onUnexpectedError(error);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
this.toUnhook.push(linkGesture.onCancel(() => {
|
||||
this.removeDecorations();
|
||||
this.currentWordUnderMouse = null;
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
private startFindDefinition(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): void {
|
||||
if (!this.isEnabled(mouseEvent, withKey)) {
|
||||
this.currentWordUnderMouse = null;
|
||||
this.removeDecorations();
|
||||
return;
|
||||
}
|
||||
|
||||
// Find word at mouse position
|
||||
let position = mouseEvent.target.position;
|
||||
let word = position ? this.editor.getModel().getWordAtPosition(position) : null;
|
||||
if (!word) {
|
||||
this.currentWordUnderMouse = null;
|
||||
this.removeDecorations();
|
||||
return;
|
||||
}
|
||||
|
||||
// Return early if word at position is still the same
|
||||
if (this.currentWordUnderMouse && this.currentWordUnderMouse.startColumn === word.startColumn && this.currentWordUnderMouse.endColumn === word.endColumn && this.currentWordUnderMouse.word === word.word) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentWordUnderMouse = word;
|
||||
|
||||
// Find definition and decorate word if found
|
||||
let state = new EditorState(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection | CodeEditorStateFlag.Scroll);
|
||||
|
||||
this.throttler.queue(() => {
|
||||
return state.validate(this.editor)
|
||||
? this.findDefinition(mouseEvent.target)
|
||||
: TPromise.as<Location[]>(null);
|
||||
|
||||
}).then(results => {
|
||||
if (!results || !results.length || !state.validate(this.editor)) {
|
||||
this.removeDecorations();
|
||||
return;
|
||||
}
|
||||
|
||||
// Multiple results
|
||||
if (results.length > 1) {
|
||||
this.addDecoration(
|
||||
new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn),
|
||||
new MarkdownString().appendText(nls.localize('multipleResults', "Click to show {0} definitions.", results.length))
|
||||
);
|
||||
}
|
||||
|
||||
// Single result
|
||||
else {
|
||||
let result = results[0];
|
||||
|
||||
if (!result.uri) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.textModelResolverService.createModelReference(result.uri).then(ref => {
|
||||
|
||||
if (!ref.object || !ref.object.textEditorModel) {
|
||||
ref.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
const { object: { textEditorModel } } = ref;
|
||||
const { startLineNumber } = result.range;
|
||||
|
||||
if (textEditorModel.getLineMaxColumn(startLineNumber) === 0) {
|
||||
ref.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber);
|
||||
const maxLineNumber = Math.min(textEditorModel.getLineCount(), startLineNumber + GotoDefinitionWithMouseEditorContribution.MAX_SOURCE_PREVIEW_LINES);
|
||||
let endLineNumber = startLineNumber + 1;
|
||||
let minIndent = startIndent;
|
||||
|
||||
for (; endLineNumber < maxLineNumber; endLineNumber++) {
|
||||
let endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber);
|
||||
minIndent = Math.min(minIndent, endIndent);
|
||||
if (startIndent === endIndent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const previewRange = new Range(startLineNumber, 1, endLineNumber + 1, 1);
|
||||
const value = textEditorModel.getValueInRange(previewRange).replace(new RegExp(`^\\s{${minIndent - 1}}`, 'gm'), '').trim();
|
||||
|
||||
this.addDecoration(
|
||||
new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn),
|
||||
new MarkdownString().appendCodeblock(this.modeService.getModeIdByFilenameOrFirstLine(textEditorModel.uri.fsPath), value)
|
||||
);
|
||||
ref.dispose();
|
||||
});
|
||||
}
|
||||
}).done(undefined, onUnexpectedError);
|
||||
}
|
||||
|
||||
private addDecoration(range: Range, hoverMessage: MarkdownString): void {
|
||||
|
||||
const newDecorations: editorCommon.IModelDeltaDecoration = {
|
||||
range: range,
|
||||
options: {
|
||||
inlineClassName: 'goto-definition-link',
|
||||
hoverMessage
|
||||
}
|
||||
};
|
||||
|
||||
this.decorations = this.editor.deltaDecorations(this.decorations, [newDecorations]);
|
||||
}
|
||||
|
||||
private removeDecorations(): void {
|
||||
if (this.decorations.length > 0) {
|
||||
this.decorations = this.editor.deltaDecorations(this.decorations, []);
|
||||
}
|
||||
}
|
||||
|
||||
private isEnabled(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): boolean {
|
||||
return this.editor.getModel() &&
|
||||
mouseEvent.isNoneOrSingleMouseDown &&
|
||||
mouseEvent.target.type === MouseTargetType.CONTENT_TEXT &&
|
||||
(mouseEvent.hasTriggerModifier || (withKey && withKey.keyCodeIsTriggerKey)) &&
|
||||
DefinitionProviderRegistry.has(this.editor.getModel());
|
||||
}
|
||||
|
||||
private findDefinition(target: IMouseTarget): TPromise<Location[]> {
|
||||
let model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return TPromise.as(null);
|
||||
}
|
||||
|
||||
return getDefinitionsAtPosition(this.editor.getModel(), target.position);
|
||||
}
|
||||
|
||||
private gotoDefinition(target: IMouseTarget, sideBySide: boolean): TPromise<any> {
|
||||
this.editor.setPosition(target.position);
|
||||
const action = new DefinitionAction(new DefinitionActionConfig(sideBySide, false, true, false), { alias: undefined, label: undefined, id: undefined, precondition: undefined });
|
||||
return this.editor.invokeWithinContext(accessor => action.run(accessor, this.editor));
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return GotoDefinitionWithMouseEditorContribution.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.toUnhook = dispose(this.toUnhook);
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let activeLinkForeground = theme.getColor(editorActiveLinkForeground);
|
||||
if (activeLinkForeground) {
|
||||
collector.addRule(`.monaco-editor .goto-definition-link { color: ${activeLinkForeground} !important; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .monaco-editor-overlaymessage {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
.monaco-editor .monaco-editor-overlaymessage.fadeIn {
|
||||
animation: fadeIn 150ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
}
|
||||
.monaco-editor .monaco-editor-overlaymessage.fadeOut {
|
||||
animation: fadeOut 100ms ease-out;
|
||||
}
|
||||
|
||||
.monaco-editor .monaco-editor-overlaymessage .message {
|
||||
padding: 1px 4px;
|
||||
}
|
||||
|
||||
.monaco-editor .monaco-editor-overlaymessage .anchor {
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
border-color: transparent;
|
||||
border-style: solid;
|
||||
z-index: 1000;
|
||||
border-width: 8px;
|
||||
position: absolute;
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./messageController';
|
||||
import { setDisposableTimeout } from 'vs/base/common/async';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { commonEditorContribution, CommonEditorRegistry, EditorCommand } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { ICodeEditor, IContentWidget, IContentWidgetPosition, ContentWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
|
||||
import { IContextKeyService, RawContextKey, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { registerThemingParticipant, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService';
|
||||
import { inputValidationInfoBorder, inputValidationInfoBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
@commonEditorContribution
|
||||
export class MessageController {
|
||||
|
||||
private static _id = 'editor.contrib.messageController';
|
||||
|
||||
static CONTEXT_SNIPPET_MODE = new RawContextKey<boolean>('messageVisible', false);
|
||||
|
||||
static get(editor: editorCommon.ICommonCodeEditor): MessageController {
|
||||
return editor.getContribution<MessageController>(MessageController._id);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return MessageController._id;
|
||||
}
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _visible: IContextKey<boolean>;
|
||||
private _messageWidget: MessageWidget;
|
||||
private _messageListeners: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._visible = MessageController.CONTEXT_SNIPPET_MODE.bindTo(contextKeyService);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._visible.reset();
|
||||
}
|
||||
|
||||
showMessage(message: string, position: IPosition): void {
|
||||
|
||||
alert(message);
|
||||
|
||||
this._visible.set(true);
|
||||
dispose(this._messageWidget);
|
||||
this._messageListeners = dispose(this._messageListeners);
|
||||
this._messageWidget = new MessageWidget(this._editor, position, message);
|
||||
|
||||
// close on blur, cursor, model change, dispose
|
||||
this._messageListeners.push(this._editor.onDidBlurEditorText(() => this.closeMessage()));
|
||||
this._messageListeners.push(this._editor.onDidChangeCursorPosition(() => this.closeMessage()));
|
||||
this._messageListeners.push(this._editor.onDidDispose(() => this.closeMessage()));
|
||||
this._messageListeners.push(this._editor.onDidChangeModel(() => this.closeMessage()));
|
||||
|
||||
// close after 3s
|
||||
this._messageListeners.push(setDisposableTimeout(() => this.closeMessage(), 3000));
|
||||
|
||||
// close on mouse move
|
||||
let bounds: Range;
|
||||
this._messageListeners.push(this._editor.onMouseMove(e => {
|
||||
// outside the text area
|
||||
if (!e.target.position) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bounds) {
|
||||
// define bounding box around position and first mouse occurance
|
||||
bounds = new Range(position.lineNumber - 3, 1, e.target.position.lineNumber + 3, 1);
|
||||
} else if (!bounds.containsPosition(e.target.position)) {
|
||||
// check if position is still in bounds
|
||||
this.closeMessage();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
closeMessage(): void {
|
||||
this._visible.reset();
|
||||
this._messageListeners = dispose(this._messageListeners);
|
||||
this._messageListeners.push(MessageWidget.fadeOut(this._messageWidget));
|
||||
}
|
||||
}
|
||||
|
||||
const MessageCommand = EditorCommand.bindToContribution<MessageController>(MessageController.get);
|
||||
|
||||
|
||||
CommonEditorRegistry.registerEditorCommand(new MessageCommand({
|
||||
id: 'leaveEditorMessage',
|
||||
precondition: MessageController.CONTEXT_SNIPPET_MODE,
|
||||
handler: c => c.closeMessage(),
|
||||
kbOpts: {
|
||||
weight: CommonEditorRegistry.commandWeight(30),
|
||||
primary: KeyCode.Escape
|
||||
}
|
||||
}));
|
||||
|
||||
class MessageWidget implements IContentWidget {
|
||||
|
||||
// Editor.IContentWidget.allowEditorOverflow
|
||||
readonly allowEditorOverflow = true;
|
||||
readonly suppressMouseDown = false;
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _position: IPosition;
|
||||
private _domNode: HTMLDivElement;
|
||||
|
||||
static fadeOut(messageWidget: MessageWidget): IDisposable {
|
||||
let handle: number;
|
||||
const dispose = () => {
|
||||
messageWidget.dispose();
|
||||
clearTimeout(handle);
|
||||
messageWidget.getDomNode().removeEventListener('animationend', dispose);
|
||||
};
|
||||
handle = setTimeout(dispose, 110);
|
||||
messageWidget.getDomNode().addEventListener('animationend', dispose);
|
||||
messageWidget.getDomNode().classList.add('fadeOut');
|
||||
return { dispose };
|
||||
}
|
||||
|
||||
constructor(editor: ICodeEditor, { lineNumber, column }: IPosition, text: string) {
|
||||
|
||||
this._editor = editor;
|
||||
this._editor.revealLinesInCenterIfOutsideViewport(lineNumber, lineNumber, editorCommon.ScrollType.Smooth);
|
||||
this._position = { lineNumber, column: 1 };
|
||||
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.style.paddingLeft = `${editor.getOffsetForColumn(lineNumber, column) - 6}px`;
|
||||
this._domNode.classList.add('monaco-editor-overlaymessage');
|
||||
|
||||
const message = document.createElement('div');
|
||||
message.classList.add('message');
|
||||
message.textContent = text;
|
||||
this._domNode.appendChild(message);
|
||||
|
||||
const anchor = document.createElement('div');
|
||||
anchor.classList.add('anchor');
|
||||
this._domNode.appendChild(anchor);
|
||||
|
||||
this._editor.addContentWidget(this);
|
||||
this._domNode.classList.add('fadeIn');
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._editor.removeContentWidget(this);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'messageoverlay';
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
getPosition(): IContentWidgetPosition {
|
||||
return { position: this._position, preference: [ContentWidgetPositionPreference.ABOVE] };
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let border = theme.getColor(inputValidationInfoBorder);
|
||||
if (border) {
|
||||
let borderWidth = theme.type === HIGH_CONTRAST ? 2 : 1;
|
||||
collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .anchor { border-top-color: ${border}; }`);
|
||||
collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { border: ${borderWidth}px solid ${border}; }`);
|
||||
}
|
||||
let background = theme.getColor(inputValidationInfoBackground);
|
||||
if (background) {
|
||||
collector.addRule(`.monaco-editor .monaco-editor-overlaymessage .message { background-color: ${background}; }`);
|
||||
}
|
||||
});
|
||||
34
src/vs/editor/contrib/gotoError/browser/gotoError.css
Normal file
@@ -0,0 +1,34 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* marker zone */
|
||||
|
||||
.monaco-editor .marker-widget {
|
||||
padding-left: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget > .stale {
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget div.block {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget .title {
|
||||
display: inline-block;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget .descriptioncontainer {
|
||||
white-space: pre;
|
||||
-webkit-user-select: text;
|
||||
user-select: text;
|
||||
}
|
||||
496
src/vs/editor/contrib/gotoError/browser/gotoError.ts
Normal file
@@ -0,0 +1,496 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./gotoError';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import Severity from 'vs/base/common/severity';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IMarker, IMarkerService } from 'vs/platform/markers/common/markers';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { editorAction, ServicesAccessor, IActionOptions, EditorAction, EditorCommand, CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
|
||||
import { registerColor, oneOf } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IThemeService, ITheme } from 'vs/platform/theme/common/themeService';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { AccessibilitySupport } from 'vs/base/common/platform';
|
||||
import { editorErrorForeground, editorErrorBorder, editorWarningForeground, editorWarningBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||
|
||||
class MarkerModel {
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _markers: IMarker[];
|
||||
private _nextIdx: number;
|
||||
private _toUnbind: IDisposable[];
|
||||
private _ignoreSelectionChange: boolean;
|
||||
private _onCurrentMarkerChanged: Emitter<IMarker>;
|
||||
private _onMarkerSetChanged: Emitter<MarkerModel>;
|
||||
|
||||
constructor(editor: ICodeEditor, markers: IMarker[]) {
|
||||
this._editor = editor;
|
||||
this._markers = null;
|
||||
this._nextIdx = -1;
|
||||
this._toUnbind = [];
|
||||
this._ignoreSelectionChange = false;
|
||||
this._onCurrentMarkerChanged = new Emitter<IMarker>();
|
||||
this._onMarkerSetChanged = new Emitter<MarkerModel>();
|
||||
this.setMarkers(markers);
|
||||
|
||||
// listen on editor
|
||||
this._toUnbind.push(this._editor.onDidDispose(() => this.dispose()));
|
||||
this._toUnbind.push(this._editor.onDidChangeCursorPosition(() => {
|
||||
if (!this._ignoreSelectionChange) {
|
||||
this._nextIdx = -1;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public get onCurrentMarkerChanged() {
|
||||
return this._onCurrentMarkerChanged.event;
|
||||
}
|
||||
|
||||
public get onMarkerSetChanged() {
|
||||
return this._onMarkerSetChanged.event;
|
||||
}
|
||||
|
||||
public setMarkers(markers: IMarker[]): void {
|
||||
// assign
|
||||
this._markers = markers || [];
|
||||
|
||||
// sort markers
|
||||
this._markers.sort((left, right) => Severity.compare(left.severity, right.severity) || Range.compareRangesUsingStarts(left, right));
|
||||
|
||||
this._nextIdx = -1;
|
||||
this._onMarkerSetChanged.fire(this);
|
||||
}
|
||||
|
||||
public withoutWatchingEditorPosition(callback: () => void): void {
|
||||
this._ignoreSelectionChange = true;
|
||||
try {
|
||||
callback();
|
||||
} finally {
|
||||
this._ignoreSelectionChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _initIdx(fwd: boolean): void {
|
||||
let found = false;
|
||||
const position = this._editor.getPosition();
|
||||
for (let i = 0; i < this._markers.length; i++) {
|
||||
let range = Range.lift(this._markers[i]);
|
||||
|
||||
if (range.isEmpty()) {
|
||||
const word = this._editor.getModel().getWordAtPosition(range.getStartPosition());
|
||||
if (word) {
|
||||
range = new Range(range.startLineNumber, word.startColumn, range.startLineNumber, word.endColumn);
|
||||
}
|
||||
}
|
||||
|
||||
if (range.containsPosition(position) || position.isBeforeOrEqual(range.getStartPosition())) {
|
||||
this._nextIdx = i + (fwd ? 0 : -1);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
// after the last change
|
||||
this._nextIdx = fwd ? 0 : this._markers.length - 1;
|
||||
}
|
||||
if (this._nextIdx < 0) {
|
||||
this._nextIdx = this._markers.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
private move(fwd: boolean): void {
|
||||
if (!this.canNavigate()) {
|
||||
this._onCurrentMarkerChanged.fire(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._nextIdx === -1) {
|
||||
this._initIdx(fwd);
|
||||
|
||||
} else if (fwd) {
|
||||
this._nextIdx += 1;
|
||||
if (this._nextIdx >= this._markers.length) {
|
||||
this._nextIdx = 0;
|
||||
}
|
||||
} else {
|
||||
this._nextIdx -= 1;
|
||||
if (this._nextIdx < 0) {
|
||||
this._nextIdx = this._markers.length - 1;
|
||||
}
|
||||
}
|
||||
const marker = this._markers[this._nextIdx];
|
||||
this._onCurrentMarkerChanged.fire(marker);
|
||||
}
|
||||
|
||||
public canNavigate(): boolean {
|
||||
return this._markers.length > 0;
|
||||
}
|
||||
|
||||
public next(): void {
|
||||
this.move(true);
|
||||
}
|
||||
|
||||
public previous(): void {
|
||||
this.move(false);
|
||||
}
|
||||
|
||||
public findMarkerAtPosition(pos: Position): IMarker {
|
||||
for (const marker of this._markers) {
|
||||
if (Range.containsPosition(marker, pos)) {
|
||||
return marker;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public get total() {
|
||||
return this._markers.length;
|
||||
}
|
||||
|
||||
public indexOf(marker: IMarker): number {
|
||||
return 1 + this._markers.indexOf(marker);
|
||||
}
|
||||
|
||||
public reveal(): void {
|
||||
|
||||
if (this._nextIdx === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.withoutWatchingEditorPosition(() => {
|
||||
const pos = new Position(this._markers[this._nextIdx].startLineNumber, this._markers[this._nextIdx].startColumn);
|
||||
this._editor.setPosition(pos);
|
||||
this._editor.revealPositionInCenter(pos, editorCommon.ScrollType.Smooth);
|
||||
});
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._toUnbind = dispose(this._toUnbind);
|
||||
}
|
||||
}
|
||||
|
||||
class MessageWidget {
|
||||
|
||||
domNode: HTMLDivElement;
|
||||
lines: number = 0;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.className = 'block descriptioncontainer';
|
||||
this.domNode.setAttribute('aria-live', 'assertive');
|
||||
this.domNode.setAttribute('role', 'alert');
|
||||
container.appendChild(this.domNode);
|
||||
}
|
||||
|
||||
update({ source, message }: IMarker): void {
|
||||
this.lines = 1;
|
||||
if (source) {
|
||||
const indent = new Array(source.length + 3 + 1).join(' ');
|
||||
message = `[${source}] ` + message.replace(/\r\n|\r|\n/g, () => {
|
||||
this.lines += 1;
|
||||
return '\n' + indent;
|
||||
});
|
||||
}
|
||||
this.domNode.innerText = message;
|
||||
}
|
||||
}
|
||||
|
||||
class MarkerNavigationWidget extends ZoneWidget {
|
||||
|
||||
private _parentContainer: HTMLElement;
|
||||
private _container: HTMLElement;
|
||||
private _title: HTMLElement;
|
||||
private _message: MessageWidget;
|
||||
private _callOnDispose: IDisposable[] = [];
|
||||
private _severity: Severity;
|
||||
private _backgroundColor: Color;
|
||||
|
||||
constructor(editor: ICodeEditor, private _model: MarkerModel, private _themeService: IThemeService) {
|
||||
super(editor, { showArrow: true, showFrame: true, isAccessible: true });
|
||||
this._severity = Severity.Warning;
|
||||
this._backgroundColor = Color.white;
|
||||
|
||||
this._applyTheme(_themeService.getTheme());
|
||||
this._callOnDispose.push(_themeService.onThemeChange(this._applyTheme.bind(this)));
|
||||
|
||||
this.create();
|
||||
this._wireModelAndView();
|
||||
}
|
||||
|
||||
private _applyTheme(theme: ITheme) {
|
||||
this._backgroundColor = theme.getColor(editorMarkerNavigationBackground);
|
||||
let frameColor = theme.getColor(this._severity === Severity.Error ? editorMarkerNavigationError : editorMarkerNavigationWarning);
|
||||
this.style({
|
||||
arrowColor: frameColor,
|
||||
frameColor: frameColor
|
||||
}); // style() will trigger _applyStyles
|
||||
}
|
||||
|
||||
protected _applyStyles(): void {
|
||||
if (this._parentContainer) {
|
||||
this._parentContainer.style.backgroundColor = this._backgroundColor.toString();
|
||||
}
|
||||
super._applyStyles();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._callOnDispose = dispose(this._callOnDispose);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this._parentContainer.focus();
|
||||
}
|
||||
|
||||
protected _fillContainer(container: HTMLElement): void {
|
||||
this._parentContainer = container;
|
||||
dom.addClass(container, 'marker-widget');
|
||||
this._parentContainer.tabIndex = 0;
|
||||
this._parentContainer.setAttribute('role', 'tooltip');
|
||||
|
||||
this._container = document.createElement('div');
|
||||
container.appendChild(this._container);
|
||||
|
||||
this._title = document.createElement('div');
|
||||
this._title.className = 'block title';
|
||||
this._container.appendChild(this._title);
|
||||
|
||||
this._message = new MessageWidget(this._container);
|
||||
this.editor.applyFontInfo(this._message.domNode);
|
||||
}
|
||||
|
||||
public show(where: Position, heightInLines: number): void {
|
||||
super.show(where, heightInLines);
|
||||
if (this.editor.getConfiguration().accessibilitySupport !== AccessibilitySupport.Disabled) {
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _wireModelAndView(): void {
|
||||
// listen to events
|
||||
this._model.onCurrentMarkerChanged(this.showAtMarker, this, this._callOnDispose);
|
||||
this._model.onMarkerSetChanged(this._onMarkersChanged, this, this._callOnDispose);
|
||||
}
|
||||
|
||||
public showAtMarker(marker: IMarker): void {
|
||||
|
||||
if (!marker) {
|
||||
return;
|
||||
}
|
||||
|
||||
// update:
|
||||
// * title
|
||||
// * message
|
||||
this._container.classList.remove('stale');
|
||||
this._title.innerHTML = nls.localize('title.wo_source', "({0}/{1})", this._model.indexOf(marker), this._model.total);
|
||||
this._message.update(marker);
|
||||
|
||||
this._model.withoutWatchingEditorPosition(() => {
|
||||
// update frame color (only applied on 'show')
|
||||
this._severity = marker.severity;
|
||||
this._applyTheme(this._themeService.getTheme());
|
||||
|
||||
this.show(new Position(marker.startLineNumber, marker.startColumn), this.computeRequiredHeight());
|
||||
});
|
||||
}
|
||||
|
||||
private _onMarkersChanged(): void {
|
||||
const marker = this._model.findMarkerAtPosition(this.position);
|
||||
if (marker) {
|
||||
this._container.classList.remove('stale');
|
||||
this._message.update(marker);
|
||||
} else {
|
||||
this._container.classList.add('stale');
|
||||
}
|
||||
this._relayout();
|
||||
}
|
||||
|
||||
protected _relayout(): void {
|
||||
super._relayout(this.computeRequiredHeight());
|
||||
}
|
||||
|
||||
private computeRequiredHeight() {
|
||||
return 1 + this._message.lines;
|
||||
}
|
||||
}
|
||||
|
||||
class MarkerNavigationAction extends EditorAction {
|
||||
|
||||
private _isNext: boolean;
|
||||
|
||||
constructor(next: boolean, opts: IActionOptions) {
|
||||
super(opts);
|
||||
this._isNext = next;
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
|
||||
const telemetryService = accessor.get(ITelemetryService);
|
||||
|
||||
const controller = MarkerController.get(editor);
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = controller.getOrCreateModel();
|
||||
telemetryService.publicLog('zoneWidgetShown', { mode: 'go to error', ...editor.getTelemetryData() });
|
||||
if (model) {
|
||||
if (this._isNext) {
|
||||
model.next();
|
||||
} else {
|
||||
model.previous();
|
||||
}
|
||||
model.reveal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@editorContribution
|
||||
class MarkerController implements editorCommon.IEditorContribution {
|
||||
|
||||
private static ID = 'editor.contrib.markerController';
|
||||
|
||||
public static get(editor: editorCommon.ICommonCodeEditor): MarkerController {
|
||||
return editor.getContribution<MarkerController>(MarkerController.ID);
|
||||
}
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _model: MarkerModel;
|
||||
private _zone: MarkerNavigationWidget;
|
||||
private _callOnClose: IDisposable[] = [];
|
||||
private _markersNavigationVisible: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IMarkerService private _markerService: IMarkerService,
|
||||
@IContextKeyService private _contextKeyService: IContextKeyService,
|
||||
@IThemeService private _themeService: IThemeService
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._markersNavigationVisible = CONTEXT_MARKERS_NAVIGATION_VISIBLE.bindTo(this._contextKeyService);
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return MarkerController.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._cleanUp();
|
||||
}
|
||||
|
||||
private _cleanUp(): void {
|
||||
this._markersNavigationVisible.reset();
|
||||
this._callOnClose = dispose(this._callOnClose);
|
||||
this._zone = null;
|
||||
this._model = null;
|
||||
}
|
||||
|
||||
public getOrCreateModel(): MarkerModel {
|
||||
|
||||
if (this._model) {
|
||||
return this._model;
|
||||
}
|
||||
|
||||
const markers = this._getMarkers();
|
||||
this._model = new MarkerModel(this._editor, markers);
|
||||
this._zone = new MarkerNavigationWidget(this._editor, this._model, this._themeService);
|
||||
this._markersNavigationVisible.set(true);
|
||||
|
||||
this._callOnClose.push(this._model);
|
||||
this._callOnClose.push(this._zone);
|
||||
|
||||
this._callOnClose.push(this._editor.onDidChangeModel(() => this._cleanUp()));
|
||||
this._model.onCurrentMarkerChanged(marker => !marker && this._cleanUp(), undefined, this._callOnClose);
|
||||
this._markerService.onMarkerChanged(this._onMarkerChanged, this, this._callOnClose);
|
||||
return this._model;
|
||||
}
|
||||
|
||||
public closeMarkersNavigation(): void {
|
||||
this._cleanUp();
|
||||
this._editor.focus();
|
||||
}
|
||||
|
||||
private _onMarkerChanged(changedResources: URI[]): void {
|
||||
if (!changedResources.some(r => this._editor.getModel().uri.toString() === r.toString())) {
|
||||
return;
|
||||
}
|
||||
this._model.setMarkers(this._getMarkers());
|
||||
}
|
||||
|
||||
private _getMarkers(): IMarker[] {
|
||||
return this._markerService.read({ resource: this._editor.getModel().uri });
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class NextMarkerAction extends MarkerNavigationAction {
|
||||
constructor() {
|
||||
super(true, {
|
||||
id: 'editor.action.marker.next',
|
||||
label: nls.localize('markerAction.next.label', "Go to Next Error or Warning"),
|
||||
alias: 'Go to Next Error or Warning',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyCode.F8
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class PrevMarkerAction extends MarkerNavigationAction {
|
||||
constructor() {
|
||||
super(false, {
|
||||
id: 'editor.action.marker.prev',
|
||||
label: nls.localize('markerAction.previous.label', "Go to Previous Error or Warning"),
|
||||
alias: 'Go to Previous Error or Warning',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.Shift | KeyCode.F8
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const CONTEXT_MARKERS_NAVIGATION_VISIBLE = new RawContextKey<boolean>('markersNavigationVisible', false);
|
||||
|
||||
const MarkerCommand = EditorCommand.bindToContribution<MarkerController>(MarkerController.get);
|
||||
|
||||
CommonEditorRegistry.registerEditorCommand(new MarkerCommand({
|
||||
id: 'closeMarkersNavigation',
|
||||
precondition: CONTEXT_MARKERS_NAVIGATION_VISIBLE,
|
||||
handler: x => x.closeMarkersNavigation(),
|
||||
kbOpts: {
|
||||
weight: CommonEditorRegistry.commandWeight(50),
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyCode.Escape,
|
||||
secondary: [KeyMod.Shift | KeyCode.Escape]
|
||||
}
|
||||
}));
|
||||
|
||||
// theming
|
||||
|
||||
let errorDefault = oneOf(editorErrorForeground, editorErrorBorder);
|
||||
let warningDefault = oneOf(editorWarningForeground, editorWarningBorder);
|
||||
|
||||
export const editorMarkerNavigationError = registerColor('editorMarkerNavigationError.background', { dark: errorDefault, light: errorDefault, hc: errorDefault }, nls.localize('editorMarkerNavigationError', 'Editor marker navigation widget error color.'));
|
||||
export const editorMarkerNavigationWarning = registerColor('editorMarkerNavigationWarning.background', { dark: warningDefault, light: warningDefault, hc: warningDefault }, nls.localize('editorMarkerNavigationWarning', 'Editor marker navigation widget warning color.'));
|
||||
export const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', { dark: '#2D2D30', light: Color.white, hc: '#0C141F' }, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.'));
|
||||
77
src/vs/editor/contrib/hover/browser/hover.css
Normal file
@@ -0,0 +1,77 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor-hover {
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
z-index: 50;
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
-khtml-user-select: text;
|
||||
-moz-user-select: text;
|
||||
-o-user-select: text;
|
||||
user-select: text;
|
||||
box-sizing: initial;
|
||||
animation: fadein 100ms linear;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.monaco-editor-hover.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor-hover .monaco-editor-hover-content {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
/*
|
||||
* https://github.com/Microsoft/monaco-editor/issues/417
|
||||
* Safari 10.1, fails inherit correct visibility from parent when we change the visibility of parent element from hidden to inherit, in this particular case.
|
||||
*/
|
||||
.monaco-editor-hover .monaco-scrollable-element {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.monaco-editor-hover .hover-row {
|
||||
padding: 4px 5px;
|
||||
}
|
||||
|
||||
.monaco-editor-hover p,
|
||||
.monaco-editor-hover ul {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.monaco-editor-hover p:first-child,
|
||||
.monaco-editor-hover ul:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-editor-hover p:last-child,
|
||||
.monaco-editor-hover ul:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.monaco-editor-hover ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.monaco-editor-hover li > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.monaco-editor-hover li > ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-editor-hover code {
|
||||
border-radius: 3px;
|
||||
padding: 0 0.4em;
|
||||
}
|
||||
|
||||
.monaco-editor-hover .monaco-tokenized-source {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
222
src/vs/editor/contrib/hover/browser/hover.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'vs/css!./hover';
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { editorAction, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
|
||||
import { ModesContentHoverWidget } from './modesContentHover';
|
||||
import { ModesGlyphHoverWidget } from './modesGlyphHover';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorHoverHighlight, editorHoverBackground, editorHoverBorder, textLinkForeground, textCodeBlockBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
|
||||
@editorContribution
|
||||
export class ModesHoverController implements editorCommon.IEditorContribution {
|
||||
|
||||
private static ID = 'editor.contrib.hover';
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _toUnhook: IDisposable[];
|
||||
|
||||
private _contentWidget: ModesContentHoverWidget;
|
||||
private _glyphWidget: ModesGlyphHoverWidget;
|
||||
|
||||
private _isMouseDown: boolean;
|
||||
private _hoverClicked: boolean;
|
||||
|
||||
static get(editor: editorCommon.ICommonCodeEditor): ModesHoverController {
|
||||
return editor.getContribution<ModesHoverController>(ModesHoverController.ID);
|
||||
}
|
||||
|
||||
constructor(editor: ICodeEditor,
|
||||
@IOpenerService openerService: IOpenerService,
|
||||
@IModeService modeService: IModeService
|
||||
) {
|
||||
this._editor = editor;
|
||||
|
||||
this._toUnhook = [];
|
||||
this._isMouseDown = false;
|
||||
|
||||
if (editor.getConfiguration().contribInfo.hover) {
|
||||
this._toUnhook.push(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e)));
|
||||
this._toUnhook.push(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(e)));
|
||||
this._toUnhook.push(this._editor.onMouseMove((e: IEditorMouseEvent) => this._onEditorMouseMove(e)));
|
||||
this._toUnhook.push(this._editor.onMouseLeave((e: IEditorMouseEvent) => this._hideWidgets()));
|
||||
this._toUnhook.push(this._editor.onKeyDown((e: IKeyboardEvent) => this._onKeyDown(e)));
|
||||
this._toUnhook.push(this._editor.onDidChangeModel(() => this._hideWidgets()));
|
||||
this._toUnhook.push(this._editor.onDidChangeModelDecorations(() => this._onModelDecorationsChanged()));
|
||||
this._toUnhook.push(this._editor.onDidScrollChange((e) => {
|
||||
if (e.scrollTopChanged || e.scrollLeftChanged) {
|
||||
this._hideWidgets();
|
||||
}
|
||||
}));
|
||||
|
||||
this._contentWidget = new ModesContentHoverWidget(editor, openerService, modeService);
|
||||
this._glyphWidget = new ModesGlyphHoverWidget(editor, openerService, modeService);
|
||||
}
|
||||
}
|
||||
|
||||
private _onModelDecorationsChanged(): void {
|
||||
this._contentWidget.onModelDecorationsChanged();
|
||||
this._glyphWidget.onModelDecorationsChanged();
|
||||
}
|
||||
|
||||
private _onEditorMouseDown(mouseEvent: IEditorMouseEvent): void {
|
||||
this._isMouseDown = true;
|
||||
|
||||
var targetType = mouseEvent.target.type;
|
||||
|
||||
if (targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === ModesContentHoverWidget.ID) {
|
||||
this._hoverClicked = true;
|
||||
// mouse down on top of content hover widget
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetType === MouseTargetType.OVERLAY_WIDGET && mouseEvent.target.detail === ModesGlyphHoverWidget.ID) {
|
||||
// mouse down on top of overlay hover widget
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetType !== MouseTargetType.OVERLAY_WIDGET && mouseEvent.target.detail !== ModesGlyphHoverWidget.ID) {
|
||||
this._hoverClicked = false;
|
||||
}
|
||||
|
||||
this._hideWidgets();
|
||||
}
|
||||
|
||||
private _onEditorMouseUp(mouseEvent: IEditorMouseEvent): void {
|
||||
this._isMouseDown = false;
|
||||
}
|
||||
|
||||
private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void {
|
||||
var targetType = mouseEvent.target.type;
|
||||
var stopKey = platform.isMacintosh ? 'metaKey' : 'ctrlKey';
|
||||
|
||||
if (this._isMouseDown && this._hoverClicked && this._contentWidget.isColorPickerVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetType === MouseTargetType.CONTENT_WIDGET && mouseEvent.target.detail === ModesContentHoverWidget.ID && !mouseEvent.event[stopKey]) {
|
||||
// mouse moved on top of content hover widget
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetType === MouseTargetType.OVERLAY_WIDGET && mouseEvent.target.detail === ModesGlyphHoverWidget.ID && !mouseEvent.event[stopKey]) {
|
||||
// mouse moved on top of overlay hover widget
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._editor.getConfiguration().contribInfo.hover && targetType === MouseTargetType.CONTENT_TEXT) {
|
||||
this._glyphWidget.hide();
|
||||
this._contentWidget.startShowingAt(mouseEvent.target.range, false);
|
||||
} else if (targetType === MouseTargetType.GUTTER_GLYPH_MARGIN) {
|
||||
this._contentWidget.hide();
|
||||
this._glyphWidget.startShowingAt(mouseEvent.target.position.lineNumber);
|
||||
} else {
|
||||
this._hideWidgets();
|
||||
}
|
||||
}
|
||||
|
||||
private _onKeyDown(e: IKeyboardEvent): void {
|
||||
if (e.keyCode !== KeyCode.Ctrl && e.keyCode !== KeyCode.Alt && e.keyCode !== KeyCode.Meta) {
|
||||
// Do not hide hover when Ctrl/Meta is pressed
|
||||
this._hideWidgets();
|
||||
}
|
||||
}
|
||||
|
||||
private _hideWidgets(): void {
|
||||
if (this._isMouseDown && this._hoverClicked && this._contentWidget.isColorPickerVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._glyphWidget.hide();
|
||||
this._contentWidget.hide();
|
||||
}
|
||||
|
||||
public showContentHover(range: Range, focus: boolean): void {
|
||||
this._contentWidget.startShowingAt(range, focus);
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return ModesHoverController.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._toUnhook = dispose(this._toUnhook);
|
||||
if (this._glyphWidget) {
|
||||
this._glyphWidget.dispose();
|
||||
this._glyphWidget = null;
|
||||
}
|
||||
if (this._contentWidget) {
|
||||
this._contentWidget.dispose();
|
||||
this._contentWidget = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class ShowHoverAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.showHover',
|
||||
label: nls.localize('showHover', "Show Hover"),
|
||||
alias: 'Show Hover',
|
||||
precondition: null,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_I)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
|
||||
let controller = ModesHoverController.get(editor);
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
const position = editor.getPosition();
|
||||
const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column);
|
||||
controller.showContentHover(range, true);
|
||||
}
|
||||
}
|
||||
|
||||
// theming
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let editorHoverHighlightColor = theme.getColor(editorHoverHighlight);
|
||||
if (editorHoverHighlightColor) {
|
||||
collector.addRule(`.monaco-editor .hoverHighlight { background-color: ${editorHoverHighlightColor}; }`);
|
||||
}
|
||||
let hoverBackground = theme.getColor(editorHoverBackground);
|
||||
if (hoverBackground) {
|
||||
collector.addRule(`.monaco-editor .monaco-editor-hover { background-color: ${hoverBackground}; }`);
|
||||
}
|
||||
let hoverBorder = theme.getColor(editorHoverBorder);
|
||||
if (hoverBorder) {
|
||||
collector.addRule(`.monaco-editor .monaco-editor-hover { border: 1px solid ${hoverBorder}; }`);
|
||||
collector.addRule(`.monaco-editor .monaco-editor-hover .hover-row:not(:first-child):not(:empty) { border-top: 1px solid ${hoverBorder.transparent(0.5)}; }`);
|
||||
}
|
||||
let link = theme.getColor(textLinkForeground);
|
||||
if (link) {
|
||||
collector.addRule(`.monaco-editor .monaco-editor-hover a { color: ${link}; }`);
|
||||
}
|
||||
let codeBackground = theme.getColor(textCodeBlockBackground);
|
||||
if (codeBackground) {
|
||||
collector.addRule(`.monaco-editor .monaco-editor-hover code { background-color: ${codeBackground}; }`);
|
||||
}
|
||||
|
||||
});
|
||||
189
src/vs/editor/contrib/hover/browser/hoverOperation.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
export interface IHoverComputer<Result> {
|
||||
|
||||
/**
|
||||
* Overwrite the default hover time
|
||||
*/
|
||||
getHoverTimeMillis?: () => number;
|
||||
|
||||
/**
|
||||
* This is called after half the hover time
|
||||
*/
|
||||
computeAsync?: () => TPromise<Result>;
|
||||
|
||||
/**
|
||||
* This is called after all the hover time
|
||||
*/
|
||||
computeSync?: () => Result;
|
||||
|
||||
/**
|
||||
* This is called whenever one of the compute* methods returns a truey value
|
||||
*/
|
||||
onResult: (result: Result, isFromSynchronousComputation: boolean) => void;
|
||||
|
||||
/**
|
||||
* This is what will be sent as progress/complete to the computation promise
|
||||
*/
|
||||
getResult: () => Result;
|
||||
|
||||
getResultWithLoadingMessage: () => Result;
|
||||
|
||||
}
|
||||
|
||||
const enum ComputeHoverOperationState {
|
||||
IDLE = 0,
|
||||
FIRST_WAIT = 1,
|
||||
SECOND_WAIT = 2,
|
||||
WAITING_FOR_ASYNC_COMPUTATION = 3
|
||||
}
|
||||
|
||||
export class HoverOperation<Result> {
|
||||
|
||||
static HOVER_TIME = 300;
|
||||
|
||||
private _computer: IHoverComputer<Result>;
|
||||
private _state: ComputeHoverOperationState;
|
||||
|
||||
private _firstWaitScheduler: RunOnceScheduler;
|
||||
private _secondWaitScheduler: RunOnceScheduler;
|
||||
private _loadingMessageScheduler: RunOnceScheduler;
|
||||
private _asyncComputationPromise: TPromise<void>;
|
||||
private _asyncComputationPromiseDone: boolean;
|
||||
|
||||
private _completeCallback: (r: Result) => void;
|
||||
private _errorCallback: (err: any) => void;
|
||||
private _progressCallback: (progress: any) => void;
|
||||
|
||||
constructor(computer: IHoverComputer<Result>, success: (r: Result) => void, error: (err: any) => void, progress: (progress: any) => void) {
|
||||
this._computer = computer;
|
||||
this._state = ComputeHoverOperationState.IDLE;
|
||||
|
||||
this._firstWaitScheduler = new RunOnceScheduler(() => this._triggerAsyncComputation(), this._getHoverTimeMillis() / 2);
|
||||
this._secondWaitScheduler = new RunOnceScheduler(() => this._triggerSyncComputation(), this._getHoverTimeMillis() / 2);
|
||||
this._loadingMessageScheduler = new RunOnceScheduler(() => this._showLoadingMessage(), 3 * this._getHoverTimeMillis());
|
||||
|
||||
this._asyncComputationPromise = null;
|
||||
this._asyncComputationPromiseDone = false;
|
||||
|
||||
this._completeCallback = success;
|
||||
this._errorCallback = error;
|
||||
this._progressCallback = progress;
|
||||
}
|
||||
|
||||
public getComputer(): IHoverComputer<Result> {
|
||||
return this._computer;
|
||||
}
|
||||
|
||||
private _getHoverTimeMillis(): number {
|
||||
if (this._computer.getHoverTimeMillis) {
|
||||
return this._computer.getHoverTimeMillis();
|
||||
}
|
||||
return HoverOperation.HOVER_TIME;
|
||||
}
|
||||
|
||||
private _triggerAsyncComputation(): void {
|
||||
this._state = ComputeHoverOperationState.SECOND_WAIT;
|
||||
this._secondWaitScheduler.schedule();
|
||||
|
||||
if (this._computer.computeAsync) {
|
||||
this._asyncComputationPromiseDone = false;
|
||||
this._asyncComputationPromise = this._computer.computeAsync().then((asyncResult: Result) => {
|
||||
this._asyncComputationPromiseDone = true;
|
||||
this._withAsyncResult(asyncResult);
|
||||
}, (e) => this._onError(e));
|
||||
} else {
|
||||
this._asyncComputationPromiseDone = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _triggerSyncComputation(): void {
|
||||
if (this._computer.computeSync) {
|
||||
this._computer.onResult(this._computer.computeSync(), true);
|
||||
}
|
||||
|
||||
if (this._asyncComputationPromiseDone) {
|
||||
this._state = ComputeHoverOperationState.IDLE;
|
||||
this._onComplete(this._computer.getResult());
|
||||
} else {
|
||||
this._state = ComputeHoverOperationState.WAITING_FOR_ASYNC_COMPUTATION;
|
||||
this._onProgress(this._computer.getResult());
|
||||
}
|
||||
}
|
||||
|
||||
private _showLoadingMessage(): void {
|
||||
if (this._state === ComputeHoverOperationState.WAITING_FOR_ASYNC_COMPUTATION) {
|
||||
this._onProgress(this._computer.getResultWithLoadingMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private _withAsyncResult(asyncResult: Result): void {
|
||||
if (asyncResult) {
|
||||
this._computer.onResult(asyncResult, false);
|
||||
}
|
||||
|
||||
if (this._state === ComputeHoverOperationState.WAITING_FOR_ASYNC_COMPUTATION) {
|
||||
this._state = ComputeHoverOperationState.IDLE;
|
||||
this._onComplete(this._computer.getResult());
|
||||
}
|
||||
}
|
||||
|
||||
private _onComplete(value: Result): void {
|
||||
if (this._completeCallback) {
|
||||
this._completeCallback(value);
|
||||
}
|
||||
}
|
||||
|
||||
private _onError(error: any): void {
|
||||
if (this._errorCallback) {
|
||||
this._errorCallback(error);
|
||||
} else {
|
||||
onUnexpectedError(error);
|
||||
}
|
||||
}
|
||||
|
||||
private _onProgress(value: Result): void {
|
||||
if (this._progressCallback) {
|
||||
this._progressCallback(value);
|
||||
}
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
if (this._state === ComputeHoverOperationState.IDLE) {
|
||||
this._state = ComputeHoverOperationState.FIRST_WAIT;
|
||||
this._firstWaitScheduler.schedule();
|
||||
this._loadingMessageScheduler.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this._loadingMessageScheduler.cancel();
|
||||
if (this._state === ComputeHoverOperationState.FIRST_WAIT) {
|
||||
this._firstWaitScheduler.cancel();
|
||||
}
|
||||
if (this._state === ComputeHoverOperationState.SECOND_WAIT) {
|
||||
this._secondWaitScheduler.cancel();
|
||||
if (this._asyncComputationPromise) {
|
||||
this._asyncComputationPromise.cancel();
|
||||
this._asyncComputationPromise = null;
|
||||
}
|
||||
}
|
||||
if (this._state === ComputeHoverOperationState.WAITING_FOR_ASYNC_COMPUTATION) {
|
||||
if (this._asyncComputationPromise) {
|
||||
this._asyncComputationPromise.cancel();
|
||||
this._asyncComputationPromise = null;
|
||||
}
|
||||
}
|
||||
this._state = ComputeHoverOperationState.IDLE;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
251
src/vs/editor/contrib/hover/browser/hoverWidgets.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { toggleClass } from 'vs/base/browser/dom';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import * as editorBrowser from 'vs/editor/browser/editorBrowser';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { IConfigurationChangedEvent } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
export class ContentHoverWidget extends Widget implements editorBrowser.IContentWidget {
|
||||
|
||||
private _id: string;
|
||||
protected _editor: editorBrowser.ICodeEditor;
|
||||
private _isVisible: boolean;
|
||||
private _containerDomNode: HTMLElement;
|
||||
private _domNode: HTMLElement;
|
||||
protected _showAtPosition: Position;
|
||||
private _stoleFocus: boolean;
|
||||
private scrollbar: DomScrollableElement;
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
// Editor.IContentWidget.allowEditorOverflow
|
||||
public allowEditorOverflow = true;
|
||||
|
||||
protected get isVisible(): boolean {
|
||||
return this._isVisible;
|
||||
}
|
||||
|
||||
protected set isVisible(value: boolean) {
|
||||
this._isVisible = value;
|
||||
toggleClass(this._containerDomNode, 'hidden', !this._isVisible);
|
||||
}
|
||||
|
||||
constructor(id: string, editor: editorBrowser.ICodeEditor) {
|
||||
super();
|
||||
this._id = id;
|
||||
this._editor = editor;
|
||||
this._isVisible = false;
|
||||
|
||||
this._containerDomNode = document.createElement('div');
|
||||
this._containerDomNode.className = 'monaco-editor-hover hidden';
|
||||
this._containerDomNode.tabIndex = 0;
|
||||
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = 'monaco-editor-hover-content';
|
||||
|
||||
this.scrollbar = new DomScrollableElement(this._domNode, {});
|
||||
this.disposables.push(this.scrollbar);
|
||||
this._containerDomNode.appendChild(this.scrollbar.getDomNode());
|
||||
|
||||
this.onkeydown(this._containerDomNode, (e: IKeyboardEvent) => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
this._register(this._editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
|
||||
if (e.fontInfo) {
|
||||
this.updateFont();
|
||||
}
|
||||
}));
|
||||
|
||||
this._editor.onDidLayoutChange(e => this.updateMaxHeight());
|
||||
|
||||
this.updateMaxHeight();
|
||||
this._editor.addContentWidget(this);
|
||||
this._showAtPosition = null;
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._containerDomNode;
|
||||
}
|
||||
|
||||
public showAt(position: Position, focus: boolean): void {
|
||||
// Position has changed
|
||||
this._showAtPosition = new Position(position.lineNumber, position.column);
|
||||
this.isVisible = true;
|
||||
|
||||
this._editor.layoutContentWidget(this);
|
||||
// Simply force a synchronous render on the editor
|
||||
// such that the widget does not really render with left = '0px'
|
||||
this._editor.render();
|
||||
this._stoleFocus = focus;
|
||||
if (focus) {
|
||||
this._containerDomNode.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (!this.isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isVisible = false;
|
||||
|
||||
this._editor.layoutContentWidget(this);
|
||||
if (this._stoleFocus) {
|
||||
this._editor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
public getPosition(): editorBrowser.IContentWidgetPosition {
|
||||
if (this.isVisible) {
|
||||
return {
|
||||
position: this._showAtPosition,
|
||||
preference: [
|
||||
editorBrowser.ContentWidgetPositionPreference.ABOVE,
|
||||
editorBrowser.ContentWidgetPositionPreference.BELOW
|
||||
]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._editor.removeContentWidget(this);
|
||||
this.disposables = dispose(this.disposables);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private updateFont(): void {
|
||||
const codeTags: HTMLElement[] = Array.prototype.slice.call(this._domNode.getElementsByTagName('code'));
|
||||
const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._domNode.getElementsByClassName('code'));
|
||||
|
||||
[...codeTags, ...codeClasses].forEach(node => this._editor.applyFontInfo(node));
|
||||
}
|
||||
|
||||
protected updateContents(node: Node): void {
|
||||
this._domNode.textContent = '';
|
||||
this._domNode.appendChild(node);
|
||||
this.updateFont();
|
||||
|
||||
this._editor.layoutContentWidget(this);
|
||||
this.scrollbar.scanDomNode();
|
||||
}
|
||||
|
||||
private updateMaxHeight(): void {
|
||||
const height = Math.max(this._editor.getLayoutInfo().height / 4, 250);
|
||||
const { fontSize, lineHeight } = this._editor.getConfiguration().fontInfo;
|
||||
|
||||
this._domNode.style.fontSize = `${fontSize}px`;
|
||||
this._domNode.style.lineHeight = `${lineHeight}px`;
|
||||
this._domNode.style.maxHeight = `${height}px`;
|
||||
}
|
||||
}
|
||||
|
||||
export class GlyphHoverWidget extends Widget implements editorBrowser.IOverlayWidget {
|
||||
|
||||
private _id: string;
|
||||
protected _editor: editorBrowser.ICodeEditor;
|
||||
private _isVisible: boolean;
|
||||
private _domNode: HTMLElement;
|
||||
protected _showAtLineNumber: number;
|
||||
|
||||
constructor(id: string, editor: editorBrowser.ICodeEditor) {
|
||||
super();
|
||||
this._id = id;
|
||||
this._editor = editor;
|
||||
this._isVisible = false;
|
||||
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = 'monaco-editor-hover hidden';
|
||||
this._domNode.setAttribute('aria-hidden', 'true');
|
||||
this._domNode.setAttribute('role', 'presentation');
|
||||
|
||||
this._showAtLineNumber = -1;
|
||||
|
||||
this._register(this._editor.onDidChangeConfiguration((e: IConfigurationChangedEvent) => {
|
||||
if (e.fontInfo) {
|
||||
this.updateFont();
|
||||
}
|
||||
}));
|
||||
|
||||
this._editor.addOverlayWidget(this);
|
||||
}
|
||||
|
||||
protected get isVisible(): boolean {
|
||||
return this._isVisible;
|
||||
}
|
||||
|
||||
protected set isVisible(value: boolean) {
|
||||
this._isVisible = value;
|
||||
toggleClass(this._domNode, 'hidden', !this._isVisible);
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
public getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
public showAt(lineNumber: number): void {
|
||||
this._showAtLineNumber = lineNumber;
|
||||
|
||||
if (!this.isVisible) {
|
||||
this.isVisible = true;
|
||||
}
|
||||
|
||||
const editorLayout = this._editor.getLayoutInfo();
|
||||
const topForLineNumber = this._editor.getTopForLineNumber(this._showAtLineNumber);
|
||||
const editorScrollTop = this._editor.getScrollTop();
|
||||
const lineHeight = this._editor.getConfiguration().lineHeight;
|
||||
const nodeHeight = this._domNode.clientHeight;
|
||||
const top = topForLineNumber - editorScrollTop - ((nodeHeight - lineHeight) / 2);
|
||||
|
||||
this._domNode.style.left = `${editorLayout.glyphMarginLeft + editorLayout.glyphMarginWidth}px`;
|
||||
this._domNode.style.top = `${Math.max(Math.round(top), 0)}px`;
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (!this.isVisible) {
|
||||
return;
|
||||
}
|
||||
this.isVisible = false;
|
||||
}
|
||||
|
||||
public getPosition(): editorBrowser.IOverlayWidgetPosition {
|
||||
return null;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._editor.removeOverlayWidget(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private updateFont(): void {
|
||||
const codeTags: HTMLElement[] = Array.prototype.slice.call(this._domNode.getElementsByTagName('code'));
|
||||
const codeClasses: HTMLElement[] = Array.prototype.slice.call(this._domNode.getElementsByClassName('code'));
|
||||
|
||||
[...codeTags, ...codeClasses].forEach(node => this._editor.applyFontInfo(node));
|
||||
}
|
||||
|
||||
protected updateContents(node: Node): void {
|
||||
this._domNode.textContent = '';
|
||||
this._domNode.appendChild(node);
|
||||
this.updateFont();
|
||||
}
|
||||
}
|
||||
397
src/vs/editor/contrib/hover/browser/modesContentHover.ts
Normal file
@@ -0,0 +1,397 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer';
|
||||
import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { HoverProviderRegistry, Hover, IColor, IColorFormatter } from 'vs/editor/common/modes';
|
||||
import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { getHover } from '../common/hover';
|
||||
import { HoverOperation, IHoverComputer } from './hoverOperation';
|
||||
import { ContentHoverWidget } from './hoverWidgets';
|
||||
import { IMarkdownString, MarkdownString, isEmptyMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations';
|
||||
import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/browser/colorPickerModel';
|
||||
import { ColorPickerWidget } from 'vs/editor/contrib/colorPicker/browser/colorPickerWidget';
|
||||
import { ColorDetector } from 'vs/editor/contrib/colorPicker/browser/colorDetector';
|
||||
import { Color, RGBA } from 'vs/base/common/color';
|
||||
import { IDisposable, empty as EmptyDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
|
||||
const $ = dom.$;
|
||||
|
||||
class ColorHover {
|
||||
|
||||
constructor(
|
||||
public readonly range: IRange,
|
||||
public readonly color: IColor,
|
||||
public readonly formatters: IColorFormatter[]
|
||||
) { }
|
||||
}
|
||||
|
||||
type HoverPart = Hover | ColorHover;
|
||||
|
||||
class ModesContentComputer implements IHoverComputer<HoverPart[]> {
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _result: HoverPart[];
|
||||
private _range: Range;
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
this._editor = editor;
|
||||
this._range = null;
|
||||
}
|
||||
|
||||
setRange(range: Range): void {
|
||||
this._range = range;
|
||||
this._result = [];
|
||||
}
|
||||
|
||||
clearResult(): void {
|
||||
this._result = [];
|
||||
}
|
||||
|
||||
computeAsync(): TPromise<HoverPart[]> {
|
||||
const model = this._editor.getModel();
|
||||
|
||||
if (!HoverProviderRegistry.has(model)) {
|
||||
return TPromise.as(null);
|
||||
}
|
||||
|
||||
return getHover(model, new Position(
|
||||
this._range.startLineNumber,
|
||||
this._range.startColumn
|
||||
));
|
||||
}
|
||||
|
||||
computeSync(): HoverPart[] {
|
||||
const lineNumber = this._range.startLineNumber;
|
||||
|
||||
if (lineNumber > this._editor.getModel().getLineCount()) {
|
||||
// Illegal line number => no results
|
||||
return [];
|
||||
}
|
||||
|
||||
const colorDetector = ColorDetector.get(this._editor);
|
||||
const maxColumn = this._editor.getModel().getLineMaxColumn(lineNumber);
|
||||
const lineDecorations = this._editor.getLineDecorations(lineNumber);
|
||||
let didFindColor = false;
|
||||
|
||||
const result = lineDecorations.map(d => {
|
||||
const startColumn = (d.range.startLineNumber === lineNumber) ? d.range.startColumn : 1;
|
||||
const endColumn = (d.range.endLineNumber === lineNumber) ? d.range.endColumn : maxColumn;
|
||||
|
||||
if (startColumn > this._range.startColumn || this._range.endColumn > endColumn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const range = new Range(this._range.startLineNumber, startColumn, this._range.startLineNumber, endColumn);
|
||||
const colorRange = colorDetector.getColorRange(d.range.getStartPosition());
|
||||
|
||||
if (!didFindColor && colorRange) {
|
||||
didFindColor = true;
|
||||
|
||||
const { color, formatters } = colorRange;
|
||||
return new ColorHover(d.range, color, formatters);
|
||||
} else {
|
||||
if (isEmptyMarkdownString(d.options.hoverMessage)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let contents: IMarkdownString[];
|
||||
|
||||
if (d.options.hoverMessage) {
|
||||
if (Array.isArray(d.options.hoverMessage)) {
|
||||
contents = [...d.options.hoverMessage];
|
||||
} else {
|
||||
contents = [d.options.hoverMessage];
|
||||
}
|
||||
}
|
||||
|
||||
return { contents, range };
|
||||
}
|
||||
});
|
||||
|
||||
return result.filter(d => !!d);
|
||||
}
|
||||
|
||||
onResult(result: HoverPart[], isFromSynchronousComputation: boolean): void {
|
||||
// Always put synchronous messages before asynchronous ones
|
||||
if (isFromSynchronousComputation) {
|
||||
this._result = result.concat(this._result.sort((a, b) => {
|
||||
if (a instanceof ColorHover) { // sort picker messages at to the top
|
||||
return -1;
|
||||
} else if (b instanceof ColorHover) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}));
|
||||
} else {
|
||||
this._result = this._result.concat(result);
|
||||
}
|
||||
}
|
||||
|
||||
getResult(): HoverPart[] {
|
||||
return this._result.slice(0);
|
||||
}
|
||||
|
||||
getResultWithLoadingMessage(): HoverPart[] {
|
||||
return this._result.slice(0).concat([this._getLoadingMessage()]);
|
||||
}
|
||||
|
||||
private _getLoadingMessage(): HoverPart {
|
||||
return {
|
||||
range: this._range,
|
||||
contents: [new MarkdownString().appendText(nls.localize('modesContentHover.loading', "Loading..."))]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class ModesContentHoverWidget extends ContentHoverWidget {
|
||||
|
||||
static ID = 'editor.contrib.modesContentHoverWidget';
|
||||
|
||||
private _messages: HoverPart[];
|
||||
private _lastRange: Range;
|
||||
private _computer: ModesContentComputer;
|
||||
private _hoverOperation: HoverOperation<HoverPart[]>;
|
||||
private _highlightDecorations: string[];
|
||||
private _isChangingDecorations: boolean;
|
||||
private _openerService: IOpenerService;
|
||||
private _modeService: IModeService;
|
||||
private _shouldFocus: boolean;
|
||||
private _colorPicker: ColorPickerWidget;
|
||||
|
||||
private renderDisposable: IDisposable = EmptyDisposable;
|
||||
private toDispose: IDisposable[];
|
||||
|
||||
constructor(editor: ICodeEditor, openerService: IOpenerService, modeService: IModeService) {
|
||||
super(ModesContentHoverWidget.ID, editor);
|
||||
|
||||
this._computer = new ModesContentComputer(this._editor);
|
||||
this._highlightDecorations = [];
|
||||
this._isChangingDecorations = false;
|
||||
this._openerService = openerService || NullOpenerService;
|
||||
this._modeService = modeService;
|
||||
|
||||
this._hoverOperation = new HoverOperation(
|
||||
this._computer,
|
||||
result => this._withResult(result, true),
|
||||
null,
|
||||
result => this._withResult(result, false)
|
||||
);
|
||||
|
||||
this.toDispose = [];
|
||||
this.toDispose.push(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.FOCUS, () => {
|
||||
if (this._colorPicker) {
|
||||
dom.addClass(this.getDomNode(), 'colorpicker-hover');
|
||||
}
|
||||
}));
|
||||
this.toDispose.push(dom.addStandardDisposableListener(this.getDomNode(), dom.EventType.BLUR, () => {
|
||||
dom.removeClass(this.getDomNode(), 'colorpicker-hover');
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.renderDisposable.dispose();
|
||||
this.renderDisposable = EmptyDisposable;
|
||||
this._hoverOperation.cancel();
|
||||
this.toDispose = dispose(this.toDispose);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
onModelDecorationsChanged(): void {
|
||||
if (this._isChangingDecorations) {
|
||||
return;
|
||||
}
|
||||
if (this.isVisible) {
|
||||
// The decorations have changed and the hover is visible,
|
||||
// we need to recompute the displayed text
|
||||
this._hoverOperation.cancel();
|
||||
this._computer.clearResult();
|
||||
|
||||
if (!this._colorPicker) { // TODO@Michel ensure that displayed text for other decorations is computed even if color picker is in place
|
||||
this._hoverOperation.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startShowingAt(range: Range, focus: boolean): void {
|
||||
if (this._lastRange && this._lastRange.equalsRange(range)) {
|
||||
// We have to show the widget at the exact same range as before, so no work is needed
|
||||
return;
|
||||
}
|
||||
|
||||
this._hoverOperation.cancel();
|
||||
|
||||
if (this.isVisible) {
|
||||
// The range might have changed, but the hover is visible
|
||||
// Instead of hiding it completely, filter out messages that are still in the new range and
|
||||
// kick off a new computation
|
||||
if (this._showAtPosition.lineNumber !== range.startLineNumber) {
|
||||
this.hide();
|
||||
} else {
|
||||
var filteredMessages: HoverPart[] = [];
|
||||
for (var i = 0, len = this._messages.length; i < len; i++) {
|
||||
var msg = this._messages[i];
|
||||
var rng = msg.range;
|
||||
if (rng.startColumn <= range.startColumn && rng.endColumn >= range.endColumn) {
|
||||
filteredMessages.push(msg);
|
||||
}
|
||||
}
|
||||
if (filteredMessages.length > 0) {
|
||||
this._renderMessages(range, filteredMessages);
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._lastRange = range;
|
||||
this._computer.setRange(range);
|
||||
this._shouldFocus = focus;
|
||||
this._hoverOperation.start();
|
||||
}
|
||||
|
||||
hide(): void {
|
||||
this._lastRange = null;
|
||||
this._hoverOperation.cancel();
|
||||
super.hide();
|
||||
this._isChangingDecorations = true;
|
||||
this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, []);
|
||||
this._isChangingDecorations = false;
|
||||
this.renderDisposable.dispose();
|
||||
this.renderDisposable = EmptyDisposable;
|
||||
this._colorPicker = null;
|
||||
}
|
||||
|
||||
isColorPickerVisible(): boolean {
|
||||
if (this._colorPicker) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _withResult(result: HoverPart[], complete: boolean): void {
|
||||
this._messages = result;
|
||||
|
||||
if (this._lastRange && this._messages.length > 0) {
|
||||
this._renderMessages(this._lastRange, this._messages);
|
||||
} else if (complete) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private _renderMessages(renderRange: Range, messages: HoverPart[]): void {
|
||||
this.renderDisposable.dispose();
|
||||
this._colorPicker = null;
|
||||
|
||||
// update column from which to show
|
||||
var renderColumn = Number.MAX_VALUE,
|
||||
highlightRange = messages[0].range,
|
||||
fragment = document.createDocumentFragment();
|
||||
|
||||
messages.forEach((msg) => {
|
||||
if (!msg.range) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderColumn = Math.min(renderColumn, msg.range.startColumn);
|
||||
highlightRange = Range.plusRange(highlightRange, msg.range);
|
||||
|
||||
if (!(msg instanceof ColorHover)) {
|
||||
msg.contents
|
||||
.filter(contents => !isEmptyMarkdownString(contents))
|
||||
.forEach(contents => {
|
||||
const renderedContents = renderMarkdown(contents, {
|
||||
actionCallback: (content) => {
|
||||
this._openerService.open(URI.parse(content)).then(void 0, onUnexpectedError);
|
||||
},
|
||||
codeBlockRenderer: (languageAlias, value): string | TPromise<string> => {
|
||||
// In markdown,
|
||||
// it is possible that we stumble upon language aliases (e.g.js instead of javascript)
|
||||
// it is possible no alias is given in which case we fall back to the current editor lang
|
||||
const modeId = languageAlias
|
||||
? this._modeService.getModeIdForLanguageName(languageAlias)
|
||||
: this._editor.getModel().getLanguageIdentifier().language;
|
||||
|
||||
return this._modeService.getOrCreateMode(modeId).then(_ => {
|
||||
return tokenizeToString(value, modeId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fragment.appendChild($('div.hover-row', null, renderedContents));
|
||||
});
|
||||
} else {
|
||||
const { red, green, blue, alpha } = msg.color;
|
||||
const rgba = new RGBA(red * 255, green * 255, blue * 255, alpha);
|
||||
const color = new Color(rgba);
|
||||
|
||||
const formatters = [...msg.formatters];
|
||||
const text = this._editor.getModel().getValueInRange(msg.range);
|
||||
|
||||
let formatterIndex = 0;
|
||||
|
||||
for (let i = 0; i < formatters.length; i++) {
|
||||
if (text === formatters[i].format(msg.color)) {
|
||||
formatterIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const model = new ColorPickerModel(color, formatters, formatterIndex);
|
||||
const widget = new ColorPickerWidget(fragment, model, this._editor.getConfiguration().pixelRatio);
|
||||
|
||||
const editorModel = this._editor.getModel();
|
||||
let range = new Range(msg.range.startLineNumber, msg.range.startColumn, msg.range.endLineNumber, msg.range.endColumn);
|
||||
|
||||
const updateEditorModel = () => {
|
||||
const text = model.formatter.format({
|
||||
red: model.color.rgba.r / 255,
|
||||
green: model.color.rgba.g / 255,
|
||||
blue: model.color.rgba.b / 255,
|
||||
alpha: model.color.rgba.a
|
||||
});
|
||||
editorModel.pushEditOperations([], [{ identifier: null, range, text, forceMoveMarkers: false }], () => []);
|
||||
this._editor.pushUndoStop();
|
||||
range = range.setEndPosition(range.endLineNumber, range.startColumn + text.length);
|
||||
};
|
||||
|
||||
const colorListener = model.onColorFlushed(updateEditorModel);
|
||||
|
||||
this._colorPicker = widget;
|
||||
this.renderDisposable = combinedDisposable([colorListener, widget]);
|
||||
}
|
||||
});
|
||||
|
||||
// show
|
||||
this.showAt(new Position(renderRange.startLineNumber, renderColumn), this._shouldFocus);
|
||||
|
||||
this.updateContents(fragment);
|
||||
|
||||
if (this._colorPicker) {
|
||||
this._colorPicker.layout();
|
||||
}
|
||||
|
||||
this._isChangingDecorations = true;
|
||||
this._highlightDecorations = this._editor.deltaDecorations(this._highlightDecorations, [{
|
||||
range: highlightRange,
|
||||
options: ModesContentHoverWidget._DECORATION_OPTIONS
|
||||
}]);
|
||||
this._isChangingDecorations = false;
|
||||
}
|
||||
|
||||
private static _DECORATION_OPTIONS = ModelDecorationOptions.register({
|
||||
className: 'hoverHighlight'
|
||||
});
|
||||
}
|
||||
186
src/vs/editor/contrib/hover/browser/modesGlyphHover.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { HoverOperation, IHoverComputer } from './hoverOperation';
|
||||
import { GlyphHoverWidget } from './hoverWidgets';
|
||||
import { $ } from 'vs/base/browser/dom';
|
||||
import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer';
|
||||
import { IOpenerService, NullOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import URI from 'vs/base/common/uri';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { tokenizeToString } from 'vs/editor/common/modes/textToHtmlTokenizer';
|
||||
import { IMarkdownString, isEmptyMarkdownString } from 'vs/base/common/htmlContent';
|
||||
|
||||
export interface IHoverMessage {
|
||||
value: IMarkdownString;
|
||||
}
|
||||
|
||||
class MarginComputer implements IHoverComputer<IHoverMessage[]> {
|
||||
|
||||
private _editor: ICodeEditor;
|
||||
private _lineNumber: number;
|
||||
private _result: IHoverMessage[];
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
this._editor = editor;
|
||||
this._lineNumber = -1;
|
||||
}
|
||||
|
||||
public setLineNumber(lineNumber: number): void {
|
||||
this._lineNumber = lineNumber;
|
||||
this._result = [];
|
||||
}
|
||||
|
||||
public clearResult(): void {
|
||||
this._result = [];
|
||||
}
|
||||
|
||||
public computeSync(): IHoverMessage[] {
|
||||
|
||||
const toHoverMessage = (contents: IMarkdownString): IHoverMessage => {
|
||||
return {
|
||||
value: contents
|
||||
};
|
||||
};
|
||||
|
||||
let lineDecorations = this._editor.getLineDecorations(this._lineNumber);
|
||||
|
||||
let result: IHoverMessage[] = [];
|
||||
for (let i = 0, len = lineDecorations.length; i < len; i++) {
|
||||
let d = lineDecorations[i];
|
||||
|
||||
if (!d.options.glyphMarginClassName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hoverMessage = d.options.glyphMarginHoverMessage;
|
||||
|
||||
if (isEmptyMarkdownString(hoverMessage)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(hoverMessage)) {
|
||||
result = result.concat(hoverMessage.map(toHoverMessage));
|
||||
} else {
|
||||
result.push(toHoverMessage(hoverMessage));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public onResult(result: IHoverMessage[], isFromSynchronousComputation: boolean): void {
|
||||
this._result = this._result.concat(result);
|
||||
}
|
||||
|
||||
public getResult(): IHoverMessage[] {
|
||||
return this._result;
|
||||
}
|
||||
|
||||
public getResultWithLoadingMessage(): IHoverMessage[] {
|
||||
return this.getResult();
|
||||
}
|
||||
}
|
||||
|
||||
export class ModesGlyphHoverWidget extends GlyphHoverWidget {
|
||||
|
||||
public static ID = 'editor.contrib.modesGlyphHoverWidget';
|
||||
private _messages: IHoverMessage[];
|
||||
private _lastLineNumber: number;
|
||||
|
||||
private _computer: MarginComputer;
|
||||
private _hoverOperation: HoverOperation<IHoverMessage[]>;
|
||||
|
||||
constructor(editor: ICodeEditor, private openerService: IOpenerService, private modeService: IModeService) {
|
||||
super(ModesGlyphHoverWidget.ID, editor);
|
||||
|
||||
this.openerService = openerService || NullOpenerService;
|
||||
|
||||
this._lastLineNumber = -1;
|
||||
|
||||
this._computer = new MarginComputer(this._editor);
|
||||
|
||||
this._hoverOperation = new HoverOperation(
|
||||
this._computer,
|
||||
(result: IHoverMessage[]) => this._withResult(result),
|
||||
null,
|
||||
(result: any) => this._withResult(result)
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._hoverOperation.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public onModelDecorationsChanged(): void {
|
||||
if (this.isVisible) {
|
||||
// The decorations have changed and the hover is visible,
|
||||
// we need to recompute the displayed text
|
||||
this._hoverOperation.cancel();
|
||||
this._computer.clearResult();
|
||||
this._hoverOperation.start();
|
||||
}
|
||||
}
|
||||
|
||||
public startShowingAt(lineNumber: number): void {
|
||||
if (this._lastLineNumber === lineNumber) {
|
||||
// We have to show the widget at the exact same line number as before, so no work is needed
|
||||
return;
|
||||
}
|
||||
|
||||
this._hoverOperation.cancel();
|
||||
|
||||
this.hide();
|
||||
|
||||
this._lastLineNumber = lineNumber;
|
||||
this._computer.setLineNumber(lineNumber);
|
||||
this._hoverOperation.start();
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this._lastLineNumber = -1;
|
||||
this._hoverOperation.cancel();
|
||||
super.hide();
|
||||
}
|
||||
|
||||
public _withResult(result: IHoverMessage[]): void {
|
||||
this._messages = result;
|
||||
|
||||
if (this._messages.length > 0) {
|
||||
this._renderMessages(this._lastLineNumber, this._messages);
|
||||
} else {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
private _renderMessages(lineNumber: number, messages: IHoverMessage[]): void {
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
messages.forEach((msg) => {
|
||||
const renderedContents = renderMarkdown(msg.value, {
|
||||
actionCallback: content => this.openerService.open(URI.parse(content)).then(undefined, onUnexpectedError),
|
||||
codeBlockRenderer: (languageAlias, value): string | TPromise<string> => {
|
||||
// In markdown, it is possible that we stumble upon language aliases (e.g. js instead of javascript)
|
||||
const modeId = this.modeService.getModeIdForLanguageName(languageAlias);
|
||||
return this.modeService.getOrCreateMode(modeId).then(_ => {
|
||||
return tokenizeToString(value, modeId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
fragment.appendChild($('div.hover-row', null, renderedContents));
|
||||
});
|
||||
|
||||
this.updateContents(fragment);
|
||||
this.showAt(lineNumber);
|
||||
}
|
||||
}
|
||||
41
src/vs/editor/contrib/hover/common/hover.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IReadOnlyModel } from 'vs/editor/common/editorCommon';
|
||||
import { CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { Hover, HoverProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { asWinJsPromise } from 'vs/base/common/async';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
|
||||
export function getHover(model: IReadOnlyModel, position: Position): TPromise<Hover[]> {
|
||||
|
||||
const supports = HoverProviderRegistry.ordered(model);
|
||||
const values: Hover[] = [];
|
||||
|
||||
const promises = supports.map((support, idx) => {
|
||||
return asWinJsPromise((token) => {
|
||||
return support.provideHover(model, position, token);
|
||||
}).then((result) => {
|
||||
if (result) {
|
||||
let hasRange = (typeof result.range !== 'undefined');
|
||||
let hasHtmlContent = typeof result.contents !== 'undefined' && result.contents && result.contents.length > 0;
|
||||
if (hasRange && hasHtmlContent) {
|
||||
values[idx] = result;
|
||||
}
|
||||
}
|
||||
}, err => {
|
||||
onUnexpectedExternalError(err);
|
||||
});
|
||||
});
|
||||
|
||||
return TPromise.join(promises).then(() => coalesce(values));
|
||||
}
|
||||
|
||||
CommonEditorRegistry.registerDefaultLanguageCommand('_executeHoverProvider', getHover);
|
||||
198
src/vs/editor/contrib/inPlaceReplace/common/inPlaceReplace.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { IEditorContribution, ICommonCodeEditor, IModelDecorationsChangeAccessor } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { editorAction, ServicesAccessor, EditorAction, commonEditorContribution } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { IInplaceReplaceSupportResult } from 'vs/editor/common/modes';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { InPlaceReplaceCommand } from './inPlaceReplaceCommand';
|
||||
import { EditorState, CodeEditorStateFlag } from 'vs/editor/common/core/editorState';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { editorBracketMatchBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModelWithDecorations';
|
||||
|
||||
@commonEditorContribution
|
||||
class InPlaceReplaceController implements IEditorContribution {
|
||||
|
||||
private static ID = 'editor.contrib.inPlaceReplaceController';
|
||||
|
||||
static get(editor: ICommonCodeEditor): InPlaceReplaceController {
|
||||
return editor.getContribution<InPlaceReplaceController>(InPlaceReplaceController.ID);
|
||||
}
|
||||
|
||||
private static DECORATION = ModelDecorationOptions.register({
|
||||
className: 'valueSetReplacement'
|
||||
});
|
||||
|
||||
private editor: ICommonCodeEditor;
|
||||
private requestIdPool: number;
|
||||
private currentRequest: TPromise<IInplaceReplaceSupportResult>;
|
||||
private decorationRemover: TPromise<void>;
|
||||
private decorationIds: string[];
|
||||
private editorWorkerService: IEditorWorkerService;
|
||||
|
||||
constructor(
|
||||
editor: ICommonCodeEditor,
|
||||
@IEditorWorkerService editorWorkerService: IEditorWorkerService
|
||||
) {
|
||||
this.editor = editor;
|
||||
this.editorWorkerService = editorWorkerService;
|
||||
this.requestIdPool = 0;
|
||||
this.currentRequest = TPromise.as(<IInplaceReplaceSupportResult>null);
|
||||
this.decorationRemover = TPromise.as(<void>null);
|
||||
this.decorationIds = [];
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return InPlaceReplaceController.ID;
|
||||
}
|
||||
|
||||
public run(source: string, up: boolean): TPromise<void> {
|
||||
|
||||
// cancel any pending request
|
||||
this.currentRequest.cancel();
|
||||
|
||||
var selection = this.editor.getSelection(),
|
||||
model = this.editor.getModel(),
|
||||
modelURI = model.uri;
|
||||
|
||||
if (selection.startLineNumber !== selection.endLineNumber) {
|
||||
// Can't accept multiline selection
|
||||
return null;
|
||||
}
|
||||
|
||||
var state = new EditorState(this.editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position);
|
||||
|
||||
if (!this.editorWorkerService.canNavigateValueSet(modelURI)) {
|
||||
this.currentRequest = TPromise.as(null);
|
||||
} else {
|
||||
this.currentRequest = this.editorWorkerService.navigateValueSet(modelURI, selection, up);
|
||||
this.currentRequest = this.currentRequest.then((basicResult) => {
|
||||
if (basicResult && basicResult.range && basicResult.value) {
|
||||
return basicResult;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
return this.currentRequest.then((result: IInplaceReplaceSupportResult) => {
|
||||
|
||||
if (!result || !result.range || !result.value) {
|
||||
// No proper result
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.validate(this.editor)) {
|
||||
// state has changed
|
||||
return;
|
||||
}
|
||||
|
||||
// Selection
|
||||
var editRange = Range.lift(result.range),
|
||||
highlightRange = result.range,
|
||||
diff = result.value.length - (selection.endColumn - selection.startColumn);
|
||||
|
||||
// highlight
|
||||
highlightRange = {
|
||||
startLineNumber: highlightRange.startLineNumber,
|
||||
startColumn: highlightRange.startColumn,
|
||||
endLineNumber: highlightRange.endLineNumber,
|
||||
endColumn: highlightRange.startColumn + result.value.length
|
||||
};
|
||||
if (diff > 1) {
|
||||
selection = new Selection(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn + diff - 1);
|
||||
}
|
||||
|
||||
// Insert new text
|
||||
var command = new InPlaceReplaceCommand(editRange, selection, result.value);
|
||||
|
||||
this.editor.pushUndoStop();
|
||||
this.editor.executeCommand(source, command);
|
||||
this.editor.pushUndoStop();
|
||||
|
||||
// add decoration
|
||||
this.decorationIds = this.editor.deltaDecorations(this.decorationIds, [{
|
||||
range: highlightRange,
|
||||
options: InPlaceReplaceController.DECORATION
|
||||
}]);
|
||||
|
||||
// remove decoration after delay
|
||||
this.decorationRemover.cancel();
|
||||
this.decorationRemover = TPromise.timeout(350);
|
||||
this.decorationRemover.then(() => {
|
||||
this.editor.changeDecorations((accessor: IModelDecorationsChangeAccessor) => {
|
||||
this.decorationIds = accessor.deltaDecorations(this.decorationIds, []);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class InPlaceReplaceUp extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.inPlaceReplace.up',
|
||||
label: nls.localize('InPlaceReplaceAction.previous.label', "Replace with Previous Value"),
|
||||
alias: 'Replace with Previous Value',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_COMMA
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): TPromise<void> {
|
||||
let controller = InPlaceReplaceController.get(editor);
|
||||
if (!controller) {
|
||||
return undefined;
|
||||
}
|
||||
return controller.run(this.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
class InPlaceReplaceDown extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.inPlaceReplace.down',
|
||||
label: nls.localize('InPlaceReplaceAction.next.label', "Replace with Next Value"),
|
||||
alias: 'Replace with Next Value',
|
||||
precondition: EditorContextKeys.writable,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_DOT
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): TPromise<void> {
|
||||
let controller = InPlaceReplaceController.get(editor);
|
||||
if (!controller) {
|
||||
return undefined;
|
||||
}
|
||||
return controller.run(this.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
let border = theme.getColor(editorBracketMatchBorder);
|
||||
if (border) {
|
||||
collector.addRule(`.monaco-editor.vs .valueSetReplacement { outline: solid 2px ${border}; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
|
||||
export class InPlaceReplaceCommand implements editorCommon.ICommand {
|
||||
|
||||
private _editRange: Range;
|
||||
private _originalSelection: Selection;
|
||||
private _text: string;
|
||||
|
||||
constructor(editRange: Range, originalSelection: Selection, text: string) {
|
||||
this._editRange = editRange;
|
||||
this._originalSelection = originalSelection;
|
||||
this._text = text;
|
||||
}
|
||||
|
||||
public getEditOperations(model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder): void {
|
||||
builder.addTrackedEditOperation(this._editRange, this._text);
|
||||
}
|
||||
|
||||
public computeCursorState(model: editorCommon.ITokenizedModel, helper: editorCommon.ICursorStateComputerData): Selection {
|
||||
var inverseEditOperations = helper.getInverseEditOperations();
|
||||
var srcRange = inverseEditOperations[0].range;
|
||||
|
||||
if (!this._originalSelection.isEmpty()) {
|
||||
// Preserve selection and extends to typed text
|
||||
return new Selection(
|
||||
srcRange.endLineNumber,
|
||||
srcRange.endColumn - this._text.length,
|
||||
srcRange.endLineNumber,
|
||||
srcRange.endColumn
|
||||
);
|
||||
}
|
||||
|
||||
return new Selection(
|
||||
srcRange.endLineNumber,
|
||||
Math.min(this._originalSelection.positionColumn, srcRange.endColumn),
|
||||
srcRange.endLineNumber,
|
||||
Math.min(this._originalSelection.positionColumn, srcRange.endColumn)
|
||||
);
|
||||
}
|
||||
}
|
||||
37
src/vs/editor/contrib/indentation/common/indentUtils.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export function getSpaceCnt(str, tabSize) {
|
||||
let spacesCnt = 0;
|
||||
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (str.charAt(i) === '\t') {
|
||||
spacesCnt += tabSize;
|
||||
} else {
|
||||
spacesCnt++;
|
||||
}
|
||||
}
|
||||
|
||||
return spacesCnt;
|
||||
}
|
||||
|
||||
export function generateIndent(spacesCnt: number, tabSize, insertSpaces) {
|
||||
spacesCnt = spacesCnt < 0 ? 0 : spacesCnt;
|
||||
|
||||
let result = '';
|
||||
if (!insertSpaces) {
|
||||
let tabsCnt = Math.floor(spacesCnt / tabSize);
|
||||
spacesCnt = spacesCnt % tabSize;
|
||||
for (let i = 0; i < tabsCnt; i++) {
|
||||
result += '\t';
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < spacesCnt; i++) {
|
||||
result += ' ';
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
620
src/vs/editor/contrib/indentation/common/indentation.ts
Normal file
@@ -0,0 +1,620 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { ICommonCodeEditor, IEditorContribution, IIdentifiedSingleEditOperation, ICommand, ICursorStateComputerData, IEditOperationBuilder, ITokenizedModel, EndOfLineSequence } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { editorAction, ServicesAccessor, IActionOptions, EditorAction, commonEditorContribution } from 'vs/editor/common/editorCommonExtensions';
|
||||
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { ShiftCommand } from 'vs/editor/common/commands/shiftCommand';
|
||||
import { TextEdit, StandardTokenType } from 'vs/editor/common/modes';
|
||||
import * as IndentUtil from './indentUtils';
|
||||
|
||||
export function shiftIndent(tabSize: number, indentation: string, count?: number): string {
|
||||
count = count || 1;
|
||||
let desiredIndentCount = ShiftCommand.shiftIndentCount(indentation, indentation.length + count, tabSize);
|
||||
let newIndentation = '';
|
||||
for (let i = 0; i < desiredIndentCount; i++) {
|
||||
newIndentation += '\t';
|
||||
}
|
||||
|
||||
return newIndentation;
|
||||
}
|
||||
|
||||
export function unshiftIndent(tabSize: number, indentation: string, count?: number): string {
|
||||
count = count || 1;
|
||||
let desiredIndentCount = ShiftCommand.unshiftIndentCount(indentation, indentation.length + count, tabSize);
|
||||
let newIndentation = '';
|
||||
for (let i = 0; i < desiredIndentCount; i++) {
|
||||
newIndentation += '\t';
|
||||
}
|
||||
|
||||
return newIndentation;
|
||||
}
|
||||
|
||||
export function getReindentEditOperations(model: ITokenizedModel, startLineNumber: number, endLineNumber: number, inheritedIndent?: string): IIdentifiedSingleEditOperation[] {
|
||||
if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {
|
||||
// Model is empty
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let indentationRules = LanguageConfigurationRegistry.getIndentationRules(model.getLanguageIdentifier().id);
|
||||
if (!indentationRules) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
endLineNumber = Math.min(endLineNumber, model.getLineCount());
|
||||
|
||||
// Skip `unIndentedLinePattern` lines
|
||||
while (startLineNumber <= endLineNumber) {
|
||||
if (!indentationRules.unIndentedLinePattern) {
|
||||
break;
|
||||
}
|
||||
|
||||
let text = model.getLineContent(startLineNumber);
|
||||
if (!indentationRules.unIndentedLinePattern.test(text)) {
|
||||
break;
|
||||
}
|
||||
|
||||
startLineNumber++;
|
||||
}
|
||||
|
||||
if (startLineNumber > endLineNumber - 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let { tabSize, insertSpaces } = model.getOptions();
|
||||
let indentEdits = [];
|
||||
|
||||
// indentation being passed to lines below
|
||||
let globalIndent: string;
|
||||
|
||||
// Calculate indentation for the first line
|
||||
// If there is no passed-in indentation, we use the indentation of the first line as base.
|
||||
let currentLineText = model.getLineContent(startLineNumber);
|
||||
let adjustedLineContent = currentLineText;
|
||||
if (inheritedIndent !== undefined && inheritedIndent !== null) {
|
||||
globalIndent = inheritedIndent;
|
||||
let oldIndentation = strings.getLeadingWhitespace(currentLineText);
|
||||
|
||||
adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length);
|
||||
if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) {
|
||||
globalIndent = unshiftIndent(tabSize, globalIndent);
|
||||
adjustedLineContent = globalIndent + currentLineText.substring(oldIndentation.length);
|
||||
|
||||
}
|
||||
if (currentLineText !== adjustedLineContent) {
|
||||
indentEdits.push(EditOperation.replace(new Selection(startLineNumber, 1, startLineNumber, oldIndentation.length + 1), TextModel.normalizeIndentation(globalIndent, tabSize, insertSpaces)));
|
||||
}
|
||||
} else {
|
||||
globalIndent = strings.getLeadingWhitespace(currentLineText);
|
||||
}
|
||||
|
||||
// idealIndentForNextLine doesn't equal globalIndent when there is a line matching `indentNextLinePattern`.
|
||||
let idealIndentForNextLine: string = globalIndent;
|
||||
|
||||
if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) {
|
||||
idealIndentForNextLine = shiftIndent(tabSize, idealIndentForNextLine);
|
||||
globalIndent = shiftIndent(tabSize, globalIndent);
|
||||
}
|
||||
else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) {
|
||||
idealIndentForNextLine = shiftIndent(tabSize, idealIndentForNextLine);
|
||||
}
|
||||
|
||||
startLineNumber++;
|
||||
|
||||
// Calculate indentation adjustment for all following lines
|
||||
for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
|
||||
let text = model.getLineContent(lineNumber);
|
||||
let oldIndentation = strings.getLeadingWhitespace(text);
|
||||
let adjustedLineContent = idealIndentForNextLine + text.substring(oldIndentation.length);
|
||||
|
||||
if (indentationRules.decreaseIndentPattern && indentationRules.decreaseIndentPattern.test(adjustedLineContent)) {
|
||||
idealIndentForNextLine = unshiftIndent(tabSize, idealIndentForNextLine);
|
||||
globalIndent = unshiftIndent(tabSize, globalIndent);
|
||||
}
|
||||
|
||||
if (oldIndentation !== idealIndentForNextLine) {
|
||||
indentEdits.push(EditOperation.replace(new Selection(lineNumber, 1, lineNumber, oldIndentation.length + 1), TextModel.normalizeIndentation(idealIndentForNextLine, tabSize, insertSpaces)));
|
||||
}
|
||||
|
||||
// calculate idealIndentForNextLine
|
||||
if (indentationRules.unIndentedLinePattern && indentationRules.unIndentedLinePattern.test(text)) {
|
||||
// In reindent phase, if the line matches `unIndentedLinePattern` we inherit indentation from above lines
|
||||
// but don't change globalIndent and idealIndentForNextLine.
|
||||
continue;
|
||||
} else if (indentationRules.increaseIndentPattern && indentationRules.increaseIndentPattern.test(adjustedLineContent)) {
|
||||
globalIndent = shiftIndent(tabSize, globalIndent);
|
||||
idealIndentForNextLine = globalIndent;
|
||||
} else if (indentationRules.indentNextLinePattern && indentationRules.indentNextLinePattern.test(adjustedLineContent)) {
|
||||
idealIndentForNextLine = shiftIndent(tabSize, idealIndentForNextLine);
|
||||
} else {
|
||||
idealIndentForNextLine = globalIndent;
|
||||
}
|
||||
}
|
||||
|
||||
return indentEdits;
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class IndentationToSpacesAction extends EditorAction {
|
||||
public static ID = 'editor.action.indentationToSpaces';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: IndentationToSpacesAction.ID,
|
||||
label: nls.localize('indentationToSpaces', "Convert Indentation to Spaces"),
|
||||
alias: 'Convert Indentation to Spaces',
|
||||
precondition: EditorContextKeys.writable
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
|
||||
let model = editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
let modelOpts = model.getOptions();
|
||||
const command = new IndentationToSpacesCommand(editor.getSelection(), modelOpts.tabSize);
|
||||
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, [command]);
|
||||
editor.pushUndoStop();
|
||||
|
||||
model.updateOptions({
|
||||
insertSpaces: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class IndentationToTabsAction extends EditorAction {
|
||||
public static ID = 'editor.action.indentationToTabs';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: IndentationToTabsAction.ID,
|
||||
label: nls.localize('indentationToTabs', "Convert Indentation to Tabs"),
|
||||
alias: 'Convert Indentation to Tabs',
|
||||
precondition: EditorContextKeys.writable
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
|
||||
let model = editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
let modelOpts = model.getOptions();
|
||||
const command = new IndentationToTabsCommand(editor.getSelection(), modelOpts.tabSize);
|
||||
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, [command]);
|
||||
editor.pushUndoStop();
|
||||
|
||||
model.updateOptions({
|
||||
insertSpaces: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ChangeIndentationSizeAction extends EditorAction {
|
||||
|
||||
constructor(private insertSpaces: boolean, opts: IActionOptions) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): TPromise<void> {
|
||||
const quickOpenService = accessor.get(IQuickOpenService);
|
||||
const modelService = accessor.get(IModelService);
|
||||
|
||||
let model = editor.getModel();
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let creationOpts = modelService.getCreationOptions(model.getLanguageIdentifier().language, model.uri);
|
||||
const picks = [1, 2, 3, 4, 5, 6, 7, 8].map(n => ({
|
||||
id: n.toString(),
|
||||
label: n.toString(),
|
||||
// add description for tabSize value set in the configuration
|
||||
description: n === creationOpts.tabSize ? nls.localize('configuredTabSize', "Configured Tab Size") : null
|
||||
}));
|
||||
|
||||
// auto focus the tabSize set for the current editor
|
||||
const autoFocusIndex = Math.min(model.getOptions().tabSize - 1, 7);
|
||||
|
||||
return TPromise.timeout(50 /* quick open is sensitive to being opened so soon after another */).then(() =>
|
||||
quickOpenService.pick(picks, { placeHolder: nls.localize({ key: 'selectTabWidth', comment: ['Tab corresponds to the tab key'] }, "Select Tab Size for Current File"), autoFocus: { autoFocusIndex } }).then(pick => {
|
||||
if (pick) {
|
||||
model.updateOptions({
|
||||
tabSize: parseInt(pick.label, 10),
|
||||
insertSpaces: this.insertSpaces
|
||||
});
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class IndentUsingTabs extends ChangeIndentationSizeAction {
|
||||
|
||||
public static ID = 'editor.action.indentUsingTabs';
|
||||
|
||||
constructor() {
|
||||
super(false, {
|
||||
id: IndentUsingTabs.ID,
|
||||
label: nls.localize('indentUsingTabs', "Indent Using Tabs"),
|
||||
alias: 'Indent Using Tabs',
|
||||
precondition: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class IndentUsingSpaces extends ChangeIndentationSizeAction {
|
||||
|
||||
public static ID = 'editor.action.indentUsingSpaces';
|
||||
|
||||
constructor() {
|
||||
super(true, {
|
||||
id: IndentUsingSpaces.ID,
|
||||
label: nls.localize('indentUsingSpaces', "Indent Using Spaces"),
|
||||
alias: 'Indent Using Spaces',
|
||||
precondition: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class DetectIndentation extends EditorAction {
|
||||
|
||||
public static ID = 'editor.action.detectIndentation';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: DetectIndentation.ID,
|
||||
label: nls.localize('detectIndentation', "Detect Indentation from Content"),
|
||||
alias: 'Detect Indentation from Content',
|
||||
precondition: null
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
|
||||
const modelService = accessor.get(IModelService);
|
||||
|
||||
let model = editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
let creationOpts = modelService.getCreationOptions(model.getLanguageIdentifier().language, model.uri);
|
||||
model.detectIndentation(creationOpts.insertSpaces, creationOpts.tabSize);
|
||||
}
|
||||
}
|
||||
|
||||
@editorAction
|
||||
export class ReindentLinesAction extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.reindentlines',
|
||||
label: nls.localize('editor.reindentlines', "Reindent Lines"),
|
||||
alias: 'Reindent Lines',
|
||||
precondition: EditorContextKeys.writable
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICommonCodeEditor): void {
|
||||
let model = editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
let edits = getReindentEditOperations(model, 1, model.getLineCount());
|
||||
if (edits) {
|
||||
editor.executeEdits(this.id, edits);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class AutoIndentOnPasteCommand implements ICommand {
|
||||
|
||||
private _edits: TextEdit[];
|
||||
private _newEol: EndOfLineSequence;
|
||||
|
||||
private _initialSelection: Selection;
|
||||
private _selectionId: string;
|
||||
|
||||
constructor(edits: TextEdit[], initialSelection: Selection) {
|
||||
this._initialSelection = initialSelection;
|
||||
this._edits = [];
|
||||
this._newEol = undefined;
|
||||
|
||||
for (let edit of edits) {
|
||||
if (typeof edit.eol === 'number') {
|
||||
this._newEol = edit.eol;
|
||||
}
|
||||
if (edit.range && typeof edit.text === 'string') {
|
||||
this._edits.push(edit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getEditOperations(model: ITokenizedModel, builder: IEditOperationBuilder): void {
|
||||
for (let edit of this._edits) {
|
||||
builder.addEditOperation(Range.lift(edit.range), edit.text);
|
||||
}
|
||||
|
||||
var selectionIsSet = false;
|
||||
if (Array.isArray(this._edits) && this._edits.length === 1 && this._initialSelection.isEmpty()) {
|
||||
if (this._edits[0].range.startColumn === this._initialSelection.endColumn &&
|
||||
this._edits[0].range.startLineNumber === this._initialSelection.endLineNumber) {
|
||||
selectionIsSet = true;
|
||||
this._selectionId = builder.trackSelection(this._initialSelection, true);
|
||||
} else if (this._edits[0].range.endColumn === this._initialSelection.startColumn &&
|
||||
this._edits[0].range.endLineNumber === this._initialSelection.startLineNumber) {
|
||||
selectionIsSet = true;
|
||||
this._selectionId = builder.trackSelection(this._initialSelection, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!selectionIsSet) {
|
||||
this._selectionId = builder.trackSelection(this._initialSelection);
|
||||
}
|
||||
}
|
||||
|
||||
public computeCursorState(model: ITokenizedModel, helper: ICursorStateComputerData): Selection {
|
||||
return helper.getTrackedSelection(this._selectionId);
|
||||
}
|
||||
}
|
||||
|
||||
@commonEditorContribution
|
||||
export class AutoIndentOnPaste implements IEditorContribution {
|
||||
private static ID = 'editor.contrib.autoIndentOnPaste';
|
||||
|
||||
private editor: ICommonCodeEditor;
|
||||
private callOnDispose: IDisposable[];
|
||||
private callOnModel: IDisposable[];
|
||||
|
||||
constructor(editor: ICommonCodeEditor) {
|
||||
this.editor = editor;
|
||||
this.callOnDispose = [];
|
||||
this.callOnModel = [];
|
||||
|
||||
this.callOnDispose.push(editor.onDidChangeConfiguration(() => this.update()));
|
||||
this.callOnDispose.push(editor.onDidChangeModel(() => this.update()));
|
||||
this.callOnDispose.push(editor.onDidChangeModelLanguage(() => this.update()));
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
|
||||
// clean up
|
||||
this.callOnModel = dispose(this.callOnModel);
|
||||
|
||||
// we are disabled
|
||||
if (!this.editor.getConfiguration().autoIndent || this.editor.getConfiguration().contribInfo.formatOnPaste) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no model
|
||||
if (!this.editor.getModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.callOnModel.push(this.editor.onDidPaste((range: Range) => {
|
||||
this.trigger(range);
|
||||
}));
|
||||
}
|
||||
|
||||
private trigger(range: Range): void {
|
||||
if (this.editor.getSelections().length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this.editor.getModel();
|
||||
if (!model.isCheapToTokenize(range.getStartPosition().lineNumber)) {
|
||||
return;
|
||||
}
|
||||
const { tabSize, insertSpaces } = model.getOptions();
|
||||
this.editor.pushUndoStop();
|
||||
let textEdits: TextEdit[] = [];
|
||||
|
||||
let indentConverter = {
|
||||
shiftIndent: (indentation) => {
|
||||
let desiredIndentCount = ShiftCommand.shiftIndentCount(indentation, indentation.length + 1, tabSize);
|
||||
let newIndentation = '';
|
||||
for (let i = 0; i < desiredIndentCount; i++) {
|
||||
newIndentation += '\t';
|
||||
}
|
||||
|
||||
return newIndentation;
|
||||
},
|
||||
unshiftIndent: (indentation) => {
|
||||
let desiredIndentCount = ShiftCommand.unshiftIndentCount(indentation, indentation.length + 1, tabSize);
|
||||
let newIndentation = '';
|
||||
for (let i = 0; i < desiredIndentCount; i++) {
|
||||
newIndentation += '\t';
|
||||
}
|
||||
|
||||
return newIndentation;
|
||||
}
|
||||
};
|
||||
|
||||
let startLineNumber = range.startLineNumber;
|
||||
|
||||
while (startLineNumber <= range.endLineNumber) {
|
||||
if (this.shouldIgnoreLine(model, startLineNumber)) {
|
||||
startLineNumber++;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (startLineNumber > range.endLineNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
let firstLineText = model.getLineContent(startLineNumber);
|
||||
if (!/\S/.test(firstLineText.substring(0, range.startColumn - 1))) {
|
||||
let indentOfFirstLine = LanguageConfigurationRegistry.getGoodIndentForLine(model, model.getLanguageIdentifier().id, startLineNumber, indentConverter);
|
||||
|
||||
if (indentOfFirstLine !== null) {
|
||||
let oldIndentation = strings.getLeadingWhitespace(firstLineText);
|
||||
let newSpaceCnt = IndentUtil.getSpaceCnt(indentOfFirstLine, tabSize);
|
||||
let oldSpaceCnt = IndentUtil.getSpaceCnt(oldIndentation, tabSize);
|
||||
|
||||
if (newSpaceCnt !== oldSpaceCnt) {
|
||||
let newIndent = IndentUtil.generateIndent(newSpaceCnt, tabSize, insertSpaces);
|
||||
textEdits.push({
|
||||
range: new Range(startLineNumber, 1, startLineNumber, oldIndentation.length + 1),
|
||||
text: newIndent
|
||||
});
|
||||
firstLineText = newIndent + firstLineText.substr(oldIndentation.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (startLineNumber !== range.endLineNumber) {
|
||||
let virtualModel = {
|
||||
getLineTokens: (lineNumber: number) => {
|
||||
return model.getLineTokens(lineNumber);
|
||||
},
|
||||
getLanguageIdentifier: () => {
|
||||
return model.getLanguageIdentifier();
|
||||
},
|
||||
getLanguageIdAtPosition: (lineNumber: number, column: number) => {
|
||||
return model.getLanguageIdAtPosition(lineNumber, column);
|
||||
},
|
||||
getLineContent: (lineNumber) => {
|
||||
if (lineNumber === startLineNumber) {
|
||||
return firstLineText;
|
||||
} else {
|
||||
return model.getLineContent(lineNumber);
|
||||
}
|
||||
}
|
||||
};
|
||||
let indentOfSecondLine = LanguageConfigurationRegistry.getGoodIndentForLine(virtualModel, model.getLanguageIdentifier().id, startLineNumber + 1, indentConverter);
|
||||
if (indentOfSecondLine !== null) {
|
||||
let newSpaceCntOfSecondLine = IndentUtil.getSpaceCnt(indentOfSecondLine, tabSize);
|
||||
let oldSpaceCntOfSecondLine = IndentUtil.getSpaceCnt(strings.getLeadingWhitespace(model.getLineContent(startLineNumber + 1)), tabSize);
|
||||
|
||||
if (newSpaceCntOfSecondLine !== oldSpaceCntOfSecondLine) {
|
||||
let spaceCntOffset = newSpaceCntOfSecondLine - oldSpaceCntOfSecondLine;
|
||||
for (let i = startLineNumber + 1; i <= range.endLineNumber; i++) {
|
||||
let lineContent = model.getLineContent(i);
|
||||
let originalIndent = strings.getLeadingWhitespace(lineContent);
|
||||
let originalSpacesCnt = IndentUtil.getSpaceCnt(originalIndent, tabSize);
|
||||
let newSpacesCnt = originalSpacesCnt + spaceCntOffset;
|
||||
let newIndent = IndentUtil.generateIndent(newSpacesCnt, tabSize, insertSpaces);
|
||||
|
||||
if (newIndent !== originalIndent) {
|
||||
textEdits.push({
|
||||
range: new Range(i, 1, i, originalIndent.length + 1),
|
||||
text: newIndent
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let cmd = new AutoIndentOnPasteCommand(textEdits, this.editor.getSelection());
|
||||
this.editor.executeCommand('autoIndentOnPaste', cmd);
|
||||
this.editor.pushUndoStop();
|
||||
}
|
||||
|
||||
private shouldIgnoreLine(model: ITokenizedModel, lineNumber: number): boolean {
|
||||
model.forceTokenization(lineNumber);
|
||||
let nonWhiteSpaceColumn = model.getLineFirstNonWhitespaceColumn(lineNumber);
|
||||
if (nonWhiteSpaceColumn === 0) {
|
||||
return true;
|
||||
}
|
||||
let tokens = model.getLineTokens(lineNumber);
|
||||
if (tokens.getTokenCount() > 0) {
|
||||
let firstNonWhiteSpaceToken = tokens.findTokenAtOffset(nonWhiteSpaceColumn);
|
||||
if (firstNonWhiteSpaceToken && firstNonWhiteSpaceToken.tokenType === StandardTokenType.Comment) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getId(): string {
|
||||
return AutoIndentOnPaste.ID;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.callOnDispose = dispose(this.callOnDispose);
|
||||
this.callOnModel = dispose(this.callOnModel);
|
||||
}
|
||||
}
|
||||
|
||||
function getIndentationEditOperations(model: ITokenizedModel, builder: IEditOperationBuilder, tabSize: number, tabsToSpaces: boolean): void {
|
||||
if (model.getLineCount() === 1 && model.getLineMaxColumn(1) === 1) {
|
||||
// Model is empty
|
||||
return;
|
||||
}
|
||||
|
||||
let spaces = '';
|
||||
for (let i = 0; i < tabSize; i++) {
|
||||
spaces += ' ';
|
||||
}
|
||||
|
||||
const content = model.getLinesContent();
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
let lastIndentationColumn = model.getLineFirstNonWhitespaceColumn(i + 1);
|
||||
if (lastIndentationColumn === 0) {
|
||||
lastIndentationColumn = model.getLineMaxColumn(i + 1);
|
||||
}
|
||||
|
||||
const text = (tabsToSpaces ? content[i].substr(0, lastIndentationColumn).replace(/\t/ig, spaces) :
|
||||
content[i].substr(0, lastIndentationColumn).replace(new RegExp(spaces, 'gi'), '\t')) +
|
||||
content[i].substr(lastIndentationColumn);
|
||||
|
||||
builder.addEditOperation(new Range(i + 1, 1, i + 1, model.getLineMaxColumn(i + 1)), text);
|
||||
}
|
||||
}
|
||||
|
||||
export class IndentationToSpacesCommand implements ICommand {
|
||||
|
||||
private selectionId: string;
|
||||
|
||||
constructor(private selection: Selection, private tabSize: number) { }
|
||||
|
||||
public getEditOperations(model: ITokenizedModel, builder: IEditOperationBuilder): void {
|
||||
this.selectionId = builder.trackSelection(this.selection);
|
||||
getIndentationEditOperations(model, builder, this.tabSize, true);
|
||||
}
|
||||
|
||||
public computeCursorState(model: ITokenizedModel, helper: ICursorStateComputerData): Selection {
|
||||
return helper.getTrackedSelection(this.selectionId);
|
||||
}
|
||||
}
|
||||
|
||||
export class IndentationToTabsCommand implements ICommand {
|
||||
|
||||
private selectionId: string;
|
||||
|
||||
constructor(private selection: Selection, private tabSize: number) { }
|
||||
|
||||
public getEditOperations(model: ITokenizedModel, builder: IEditOperationBuilder): void {
|
||||
this.selectionId = builder.trackSelection(this.selection);
|
||||
getIndentationEditOperations(model, builder, this.tabSize, false);
|
||||
}
|
||||
|
||||
public computeCursorState(model: ITokenizedModel, helper: ICursorStateComputerData): Selection {
|
||||
return helper.getTrackedSelection(this.selectionId);
|
||||
}
|
||||
}
|
||||
172
src/vs/editor/contrib/indentation/test/indentation.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { IndentationToSpacesCommand, IndentationToTabsCommand } from 'vs/editor/contrib/indentation/common/indentation';
|
||||
import { testCommand } from 'vs/editor/test/common/commands/commandTestUtils';
|
||||
|
||||
function testIndentationToSpacesCommand(lines: string[], selection: Selection, tabSize: number, expectedLines: string[], expectedSelection: Selection): void {
|
||||
testCommand(lines, null, selection, (sel) => new IndentationToSpacesCommand(sel, tabSize), expectedLines, expectedSelection);
|
||||
}
|
||||
|
||||
function testIndentationToTabsCommand(lines: string[], selection: Selection, tabSize: number, expectedLines: string[], expectedSelection: Selection): void {
|
||||
testCommand(lines, null, selection, (sel) => new IndentationToTabsCommand(sel, tabSize), expectedLines, expectedSelection);
|
||||
}
|
||||
|
||||
suite('Editor Contrib - Indentation to Spaces', () => {
|
||||
|
||||
test('single tabs only at start of line', function () {
|
||||
testIndentationToSpacesCommand(
|
||||
[
|
||||
'first',
|
||||
'second line',
|
||||
'third line',
|
||||
'\tfourth line',
|
||||
'\tfifth'
|
||||
],
|
||||
new Selection(2, 3, 2, 3),
|
||||
4,
|
||||
[
|
||||
'first',
|
||||
'second line',
|
||||
'third line',
|
||||
' fourth line',
|
||||
' fifth'
|
||||
],
|
||||
new Selection(2, 3, 2, 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple tabs at start of line', function () {
|
||||
testIndentationToSpacesCommand(
|
||||
[
|
||||
'\t\tfirst',
|
||||
'\tsecond line',
|
||||
'\t\t\t third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 5, 1, 5),
|
||||
3,
|
||||
[
|
||||
' first',
|
||||
' second line',
|
||||
' third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 5, 1, 5)
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple tabs', function () {
|
||||
testIndentationToSpacesCommand(
|
||||
[
|
||||
'\t\tfirst\t',
|
||||
'\tsecond \t line \t',
|
||||
'\t\t\t third line',
|
||||
' \tfourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 5, 1, 5),
|
||||
2,
|
||||
[
|
||||
' first\t',
|
||||
' second \t line \t',
|
||||
' third line',
|
||||
' fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 5, 1, 5)
|
||||
);
|
||||
});
|
||||
|
||||
test('empty lines', function () {
|
||||
testIndentationToSpacesCommand(
|
||||
[
|
||||
'\t\t\t',
|
||||
'\t',
|
||||
'\t\t'
|
||||
],
|
||||
new Selection(1, 4, 1, 4),
|
||||
2,
|
||||
[
|
||||
' ',
|
||||
' ',
|
||||
' '
|
||||
],
|
||||
new Selection(1, 4, 1, 4)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Editor Contrib - Indentation to Tabs', () => {
|
||||
|
||||
test('spaces only at start of line', function () {
|
||||
testIndentationToTabsCommand(
|
||||
[
|
||||
' first',
|
||||
'second line',
|
||||
' third line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 3, 2, 3),
|
||||
4,
|
||||
[
|
||||
'\tfirst',
|
||||
'second line',
|
||||
'\tthird line',
|
||||
'fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(2, 3, 2, 3)
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple spaces at start of line', function () {
|
||||
testIndentationToTabsCommand(
|
||||
[
|
||||
'first',
|
||||
' second line',
|
||||
' third line',
|
||||
'fourth line',
|
||||
' fifth'
|
||||
],
|
||||
new Selection(1, 5, 1, 5),
|
||||
3,
|
||||
[
|
||||
'first',
|
||||
'\tsecond line',
|
||||
'\t\t\t third line',
|
||||
'fourth line',
|
||||
'\t fifth'
|
||||
],
|
||||
new Selection(1, 5, 1, 5)
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple spaces', function () {
|
||||
testIndentationToTabsCommand(
|
||||
[
|
||||
' first ',
|
||||
' second line \t',
|
||||
' third line',
|
||||
' fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 5, 1, 5),
|
||||
2,
|
||||
[
|
||||
'\t\t\tfirst ',
|
||||
'\tsecond line \t',
|
||||
'\t\t\t third line',
|
||||
'\t fourth line',
|
||||
'fifth'
|
||||
],
|
||||
new Selection(1, 5, 1, 5)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection, SelectionDirection } from 'vs/editor/common/core/selection';
|
||||
import * as editorCommon from 'vs/editor/common/editorCommon';
|
||||
|
||||
export class CopyLinesCommand implements editorCommon.ICommand {
|
||||
|
||||
private _selection: Selection;
|
||||
private _isCopyingDown: boolean;
|
||||
|
||||
private _selectionDirection: SelectionDirection;
|
||||
private _selectionId: string;
|
||||
private _startLineNumberDelta: number;
|
||||
private _endLineNumberDelta: number;
|
||||
|
||||
constructor(selection: Selection, isCopyingDown: boolean) {
|
||||
this._selection = selection;
|
||||
this._isCopyingDown = isCopyingDown;
|
||||
}
|
||||
|
||||
public getEditOperations(model: editorCommon.ITokenizedModel, builder: editorCommon.IEditOperationBuilder): void {
|
||||
var s = this._selection;
|
||||
|
||||
this._startLineNumberDelta = 0;
|
||||
this._endLineNumberDelta = 0;
|
||||
if (s.startLineNumber < s.endLineNumber && s.endColumn === 1) {
|
||||
this._endLineNumberDelta = 1;
|
||||
s = s.setEndPosition(s.endLineNumber - 1, model.getLineMaxColumn(s.endLineNumber - 1));
|
||||
}
|
||||
|
||||
var sourceLines: string[] = [];
|
||||
for (var i = s.startLineNumber; i <= s.endLineNumber; i++) {
|
||||
sourceLines.push(model.getLineContent(i));
|
||||
}
|
||||
var sourceText = sourceLines.join('\n');
|
||||
|
||||
if (sourceText === '') {
|
||||
// Duplicating empty line
|
||||
if (this._isCopyingDown) {
|
||||
this._startLineNumberDelta++;
|
||||
this._endLineNumberDelta++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this._isCopyingDown) {
|
||||
builder.addEditOperation(new Range(s.endLineNumber, model.getLineMaxColumn(s.endLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)), '\n' + sourceText);
|
||||
} else {
|
||||
builder.addEditOperation(new Range(s.startLineNumber, 1, s.startLineNumber, 1), sourceText + '\n');
|
||||
}
|
||||
|
||||
this._selectionId = builder.trackSelection(s);
|
||||
this._selectionDirection = this._selection.getDirection();
|
||||
}
|
||||
|
||||
public computeCursorState(model: editorCommon.ITokenizedModel, helper: editorCommon.ICursorStateComputerData): Selection {
|
||||
var result = helper.getTrackedSelection(this._selectionId);
|
||||
|
||||
if (this._startLineNumberDelta !== 0 || this._endLineNumberDelta !== 0) {
|
||||
var startLineNumber = result.startLineNumber,
|
||||
startColumn = result.startColumn,
|
||||
endLineNumber = result.endLineNumber,
|
||||
endColumn = result.endColumn;
|
||||
|
||||
if (this._startLineNumberDelta !== 0) {
|
||||
startLineNumber = startLineNumber + this._startLineNumberDelta;
|
||||
startColumn = 1;
|
||||
}
|
||||
|
||||
if (this._endLineNumberDelta !== 0) {
|
||||
endLineNumber = endLineNumber + this._endLineNumberDelta;
|
||||
endColumn = 1;
|
||||
}
|
||||
|
||||
result = Selection.createWithDirection(startLineNumber, startColumn, endLineNumber, endColumn, this._selectionDirection);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||