SQL Operations Studio Public Preview 1 (0.23) release source code

This commit is contained in:
Karl Burtram
2017-11-09 14:30:27 -08:00
parent b88ecb8d93
commit 3cdac41339
8829 changed files with 759707 additions and 286 deletions

View 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-editor .bracket-match {
box-sizing: border-box;
}

View 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}; }`);
}
});

View File

@@ -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();
});
});

View File

@@ -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
});
}
}

View File

@@ -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;
}
}

View 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();
}
}
}

View File

@@ -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)
);
});
});

View 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;
}

View 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;
}
}

View 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));
});

View 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;
});
}
}

View 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;
}

View 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 = '&nbsp;';
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 = '&nbsp;';
}
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>&nbsp;|&nbsp;</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; }`);
}
});

View 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);
}
}

View 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;
}

View File

@@ -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);
}
}
}

View 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();
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 B

View 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))));
}

View 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);
}
}

View File

@@ -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)');
});
});

View 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
);
}
}
}

View 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();
}
}

View 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;
}
}
}
}

View File

@@ -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)
);
});
});

View File

@@ -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),
);
});
});

View 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();
}
}

View 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();
}
}

View 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;
}

View 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);
}
}

View 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;
}
}

View 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();
}
}
}

View 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}; }`);
}
});

View 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);
}

File diff suppressed because it is too large Load Diff

View 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

View 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:#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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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="#C5C5C5" d="M1,4h7L5,1h3l4,4L8,9H5l3-3H1V4z"/>
</svg>

After

Width:  |  Height:  |  Size: 290 B

View 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

View 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="#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

View 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

View 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="#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

View 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

View 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="#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

View 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

View 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;
}

View 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}; }`);
}
});

View 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;
}

File diff suppressed because it is too large Load Diff

View 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
});
}

View 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;
}
}
}

View 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);
}
}
}

View 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);
}
}

View 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();
}

View 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);
});
});
});

View 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();
});
});
});

File diff suppressed because it is too large Load Diff

View 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{}');
});
});

View 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="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

View 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

View 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:#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

View 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

View 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;
}

View 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))
}
})
);
};

View 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;
}

View 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;
}
}

View 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);
}

View 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), []);
});
});

View 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;
});

View 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);
});

View 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
};
}
}

View 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)
);
});
});

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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
}
});
}
}

View File

@@ -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;
}

View File

@@ -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; }`);
}
});

View File

@@ -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;
}

View File

@@ -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}; }`);
}
});

View 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;
}

View 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.'));

View 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;
}

View 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}; }`);
}
});

View 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;
}
}

View 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();
}
}

View 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'
});
}

View 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);
}
}

View 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);

View 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}; }`);
}
});

View File

@@ -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)
);
}
}

View 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;
}

View 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);
}
}

View 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)
);
});
});

View File

@@ -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;
}
}

Some files were not shown because too many files have changed in this diff Show More