Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .selection-anchor {
|
||||
background-color: #007ACC;
|
||||
width: 2px !important;
|
||||
}
|
||||
178
lib/vscode/src/vs/editor/contrib/anchorSelect/anchorSelect.ts
Normal file
178
lib/vscode/src/vs/editor/contrib/anchorSelect/anchorSelect.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./anchorSelect';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { localize } from 'vs/nls';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { KeyMod, KeyCode, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { TrackedRangeStickiness } from 'vs/editor/common/model';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
|
||||
export const SelectionAnchorSet = new RawContextKey('selectionAnchorSet', false);
|
||||
|
||||
class SelectionAnchorController implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.selectionAnchorController';
|
||||
|
||||
static get(editor: ICodeEditor): SelectionAnchorController {
|
||||
return editor.getContribution<SelectionAnchorController>(SelectionAnchorController.ID);
|
||||
}
|
||||
|
||||
private decorationId: string | undefined;
|
||||
private selectionAnchorSetContextKey: IContextKey<boolean>;
|
||||
private modelChangeListener: IDisposable;
|
||||
|
||||
constructor(
|
||||
private editor: ICodeEditor,
|
||||
@IContextKeyService contextKeyService: IContextKeyService
|
||||
) {
|
||||
this.selectionAnchorSetContextKey = SelectionAnchorSet.bindTo(contextKeyService);
|
||||
this.modelChangeListener = editor.onDidChangeModel(() => this.selectionAnchorSetContextKey.reset());
|
||||
}
|
||||
|
||||
setSelectionAnchor(): void {
|
||||
if (this.editor.hasModel()) {
|
||||
const position = this.editor.getPosition();
|
||||
const previousDecorations = this.decorationId ? [this.decorationId] : [];
|
||||
const newDecorationId = this.editor.deltaDecorations(previousDecorations, [{
|
||||
range: Selection.fromPositions(position, position),
|
||||
options: {
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
hoverMessage: new MarkdownString().appendText(localize('selectionAnchor', "Selection Anchor")),
|
||||
className: 'selection-anchor'
|
||||
}
|
||||
}]);
|
||||
this.decorationId = newDecorationId[0];
|
||||
this.selectionAnchorSetContextKey.set(!!this.decorationId);
|
||||
alert(localize('anchorSet', "Anchor set at {0}:{1}", position.lineNumber, position.column));
|
||||
}
|
||||
}
|
||||
|
||||
goToSelectionAnchor(): void {
|
||||
if (this.editor.hasModel() && this.decorationId) {
|
||||
const anchorPosition = this.editor.getModel().getDecorationRange(this.decorationId);
|
||||
if (anchorPosition) {
|
||||
this.editor.setPosition(anchorPosition.getStartPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectFromAnchorToCursor(): void {
|
||||
if (this.editor.hasModel() && this.decorationId) {
|
||||
const start = this.editor.getModel().getDecorationRange(this.decorationId);
|
||||
if (start) {
|
||||
const end = this.editor.getPosition();
|
||||
this.editor.setSelection(Selection.fromPositions(start.getStartPosition(), end));
|
||||
this.cancelSelectionAnchor();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelSelectionAnchor(): void {
|
||||
if (this.decorationId) {
|
||||
this.editor.deltaDecorations([this.decorationId], []);
|
||||
this.decorationId = undefined;
|
||||
this.selectionAnchorSetContextKey.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.cancelSelectionAnchor();
|
||||
this.modelChangeListener.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SetSelectionAnchor extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.setSelectionAnchor',
|
||||
label: localize('setSelectionAnchor', "Set Selection Anchor"),
|
||||
alias: 'Set Selection Anchor',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_B),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
const controller = SelectionAnchorController.get(editor);
|
||||
controller.setSelectionAnchor();
|
||||
}
|
||||
}
|
||||
|
||||
class GoToSelectionAnchor extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.goToSelectionAnchor',
|
||||
label: localize('goToSelectionAnchor', "Go to Selection Anchor"),
|
||||
alias: 'Go to Selection Anchor',
|
||||
precondition: SelectionAnchorSet,
|
||||
});
|
||||
}
|
||||
|
||||
async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
const controller = SelectionAnchorController.get(editor);
|
||||
controller.goToSelectionAnchor();
|
||||
}
|
||||
}
|
||||
|
||||
class SelectFromAnchorToCursor extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.selectFromAnchorToCursor',
|
||||
label: localize('selectFromAnchorToCursor', "Select from Anchor to Cursor"),
|
||||
alias: 'Select from Anchor to Cursor',
|
||||
precondition: SelectionAnchorSet,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
const controller = SelectionAnchorController.get(editor);
|
||||
controller.selectFromAnchorToCursor();
|
||||
}
|
||||
}
|
||||
|
||||
class CancelSelectionAnchor extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.cancelSelectionAnchor',
|
||||
label: localize('cancelSelectionAnchor', "Cancel Selection Anchor"),
|
||||
alias: 'Cancel Selection Anchor',
|
||||
precondition: SelectionAnchorSet,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyCode.Escape,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
const controller = SelectionAnchorController.get(editor);
|
||||
controller.cancelSelectionAnchor();
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(SelectionAnchorController.ID, SelectionAnchorController);
|
||||
registerEditorAction(SetSelectionAnchor);
|
||||
registerEditorAction(GoToSelectionAnchor);
|
||||
registerEditorAction(SelectFromAnchorToCursor);
|
||||
registerEditorAction(CancelSelectionAnchor);
|
||||
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .bracket-match {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./bracketMatching';
|
||||
import * as nls from 'vs/nls';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
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 { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness } from 'vs/editor/common/model';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { editorBracketMatchBackground, editorBracketMatchBorder } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { registerColor } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { registerThemingParticipant, themeColorFromId } from 'vs/platform/theme/common/themeService';
|
||||
import { MenuRegistry, MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
const overviewRulerBracketMatchForeground = registerColor('editorOverviewRuler.bracketMatchForeground', { dark: '#A0A0A0', light: '#A0A0A0', hc: '#A0A0A0' }, nls.localize('overviewRulerBracketMatchForeground', 'Overview ruler marker color for matching brackets.'));
|
||||
|
||||
class JumpToBracketAction extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.jumpToBracket',
|
||||
label: nls.localize('smartSelect.jumpBracket', "Go to Bracket"),
|
||||
alias: 'Go to Bracket',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
let controller = BracketMatchingController.get(editor);
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
controller.jumpToBracket();
|
||||
}
|
||||
}
|
||||
|
||||
class SelectToBracketAction extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.selectToBracket',
|
||||
label: nls.localize('smartSelect.selectToBracket', "Select to Bracket"),
|
||||
alias: 'Select to Bracket',
|
||||
precondition: undefined,
|
||||
description: {
|
||||
description: `Select to Bracket`,
|
||||
args: [{
|
||||
name: 'args',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
'selectBrackets': {
|
||||
type: 'boolean',
|
||||
default: true
|
||||
}
|
||||
},
|
||||
}
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {
|
||||
const controller = BracketMatchingController.get(editor);
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
|
||||
let selectBrackets = true;
|
||||
if (args && args.selectBrackets === false) {
|
||||
selectBrackets = false;
|
||||
}
|
||||
controller.selectToBracket(selectBrackets);
|
||||
}
|
||||
}
|
||||
|
||||
type Brackets = [Range, Range];
|
||||
|
||||
class BracketsData {
|
||||
public readonly position: Position;
|
||||
public readonly brackets: Brackets | null;
|
||||
public readonly options: ModelDecorationOptions;
|
||||
|
||||
constructor(position: Position, brackets: Brackets | null, options: ModelDecorationOptions) {
|
||||
this.position = position;
|
||||
this.brackets = brackets;
|
||||
this.options = options;
|
||||
}
|
||||
}
|
||||
|
||||
export class BracketMatchingController extends Disposable implements IEditorContribution {
|
||||
public static readonly ID = 'editor.contrib.bracketMatchingController';
|
||||
|
||||
public static get(editor: ICodeEditor): BracketMatchingController {
|
||||
return editor.getContribution<BracketMatchingController>(BracketMatchingController.ID);
|
||||
}
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
|
||||
private _lastBracketsData: BracketsData[];
|
||||
private _lastVersionId: number;
|
||||
private _decorations: string[];
|
||||
private readonly _updateBracketsSoon: RunOnceScheduler;
|
||||
private _matchBrackets: 'never' | 'near' | 'always';
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor
|
||||
) {
|
||||
super();
|
||||
this._editor = editor;
|
||||
this._lastBracketsData = [];
|
||||
this._lastVersionId = 0;
|
||||
this._decorations = [];
|
||||
this._updateBracketsSoon = this._register(new RunOnceScheduler(() => this._updateBrackets(), 50));
|
||||
this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets);
|
||||
|
||||
this._updateBracketsSoon.schedule();
|
||||
this._register(editor.onDidChangeCursorPosition((e) => {
|
||||
|
||||
if (this._matchBrackets === 'never') {
|
||||
// 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.onDidChangeModelContent((e) => {
|
||||
this._updateBracketsSoon.schedule();
|
||||
}));
|
||||
this._register(editor.onDidChangeModel((e) => {
|
||||
this._lastBracketsData = [];
|
||||
this._decorations = [];
|
||||
this._updateBracketsSoon.schedule();
|
||||
}));
|
||||
this._register(editor.onDidChangeModelLanguageConfiguration((e) => {
|
||||
this._lastBracketsData = [];
|
||||
this._updateBracketsSoon.schedule();
|
||||
}));
|
||||
this._register(editor.onDidChangeConfiguration((e) => {
|
||||
if (e.hasChanged(EditorOption.matchBrackets)) {
|
||||
this._matchBrackets = this._editor.getOption(EditorOption.matchBrackets);
|
||||
this._decorations = this._editor.deltaDecorations(this._decorations, []);
|
||||
this._lastBracketsData = [];
|
||||
this._lastVersionId = 0;
|
||||
this._updateBracketsSoon.schedule();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public jumpToBracket(): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const 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 = 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 enclosing brackets if the position isn't on a matching bracket
|
||||
const enclosingBrackets = model.findEnclosingBrackets(position);
|
||||
if (enclosingBrackets) {
|
||||
newCursorPosition = enclosingBrackets[0].getStartPosition();
|
||||
} else {
|
||||
// no enclosing brackets, try the very first next 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);
|
||||
this._editor.revealRange(newSelections[0]);
|
||||
}
|
||||
|
||||
public selectToBracket(selectBrackets: boolean): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const newSelections: Selection[] = [];
|
||||
|
||||
this._editor.getSelections().forEach(selection => {
|
||||
const position = selection.getStartPosition();
|
||||
let brackets = model.matchBracket(position);
|
||||
|
||||
if (!brackets) {
|
||||
brackets = model.findEnclosingBrackets(position);
|
||||
if (!brackets) {
|
||||
const nextBracket = model.findNextBracket(position);
|
||||
if (nextBracket && nextBracket.range) {
|
||||
brackets = model.matchBracket(nextBracket.range.getStartPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let selectFrom: Position | null = null;
|
||||
let selectTo: Position | null = null;
|
||||
|
||||
if (brackets) {
|
||||
brackets.sort(Range.compareRangesUsingStarts);
|
||||
const [open, close] = brackets;
|
||||
selectFrom = selectBrackets ? open.getStartPosition() : open.getEndPosition();
|
||||
selectTo = selectBrackets ? close.getEndPosition() : close.getStartPosition();
|
||||
}
|
||||
|
||||
if (selectFrom && selectTo) {
|
||||
newSelections.push(new Selection(selectFrom.lineNumber, selectFrom.column, selectTo.lineNumber, selectTo.column));
|
||||
}
|
||||
});
|
||||
|
||||
if (newSelections.length > 0) {
|
||||
this._editor.setSelections(newSelections);
|
||||
this._editor.revealRange(newSelections[0]);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly _DECORATION_OPTIONS_WITH_OVERVIEW_RULER = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'bracket-match',
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(overviewRulerBracketMatchForeground),
|
||||
position: OverviewRulerLane.Center
|
||||
}
|
||||
});
|
||||
|
||||
private static readonly _DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'bracket-match'
|
||||
});
|
||||
|
||||
private _updateBrackets(): void {
|
||||
if (this._matchBrackets === 'never') {
|
||||
return;
|
||||
}
|
||||
this._recomputeBrackets();
|
||||
|
||||
let newDecorations: IModelDeltaDecoration[] = [], newDecorationsLen = 0;
|
||||
for (const bracketData of this._lastBracketsData) {
|
||||
let brackets = bracketData.brackets;
|
||||
if (brackets) {
|
||||
newDecorations[newDecorationsLen++] = { range: brackets[0], options: bracketData.options };
|
||||
newDecorations[newDecorationsLen++] = { range: brackets[1], options: bracketData.options };
|
||||
}
|
||||
}
|
||||
|
||||
this._decorations = this._editor.deltaDecorations(this._decorations, newDecorations);
|
||||
}
|
||||
|
||||
private _recomputeBrackets(): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
// no model => no brackets!
|
||||
this._lastBracketsData = [];
|
||||
this._lastVersionId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const selections = this._editor.getSelections();
|
||||
if (selections.length > 100) {
|
||||
// no bracket matching for high numbers of selections
|
||||
this._lastBracketsData = [];
|
||||
this._lastVersionId = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
let options = BracketMatchingController._DECORATION_OPTIONS_WITH_OVERVIEW_RULER;
|
||||
if (!brackets && this._matchBrackets === 'always') {
|
||||
brackets = model.findEnclosingBrackets(position, 20 /* give at most 20ms to compute */);
|
||||
options = BracketMatchingController._DECORATION_OPTIONS_WITHOUT_OVERVIEW_RULER;
|
||||
}
|
||||
newData[newDataLen++] = new BracketsData(position, brackets, options);
|
||||
}
|
||||
}
|
||||
|
||||
this._lastBracketsData = newData;
|
||||
this._lastVersionId = versionId;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(BracketMatchingController.ID, BracketMatchingController);
|
||||
registerEditorAction(SelectToBracketAction);
|
||||
registerEditorAction(JumpToBracketAction);
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const bracketMatchBackground = theme.getColor(editorBracketMatchBackground);
|
||||
if (bracketMatchBackground) {
|
||||
collector.addRule(`.monaco-editor .bracket-match { background-color: ${bracketMatchBackground}; }`);
|
||||
}
|
||||
const bracketMatchBorder = theme.getColor(editorBracketMatchBorder);
|
||||
if (bracketMatchBorder) {
|
||||
collector.addRule(`.monaco-editor .bracket-match { border: 1px solid ${bracketMatchBorder}; }`);
|
||||
}
|
||||
});
|
||||
|
||||
// Go to menu
|
||||
MenuRegistry.appendMenuItem(MenuId.MenubarGoMenu, {
|
||||
group: '5_infile_nav',
|
||||
command: {
|
||||
id: 'editor.action.jumpToBracket',
|
||||
title: nls.localize({ key: 'miGoToBracket', comment: ['&& denotes a mnemonic'] }, "Go to &&Bracket")
|
||||
},
|
||||
order: 2
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { LanguageIdentifier } from 'vs/editor/common/modes';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { BracketMatchingController } from 'vs/editor/contrib/bracketMatching/bracketMatching';
|
||||
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
import { MockMode } from 'vs/editor/test/common/mocks/mockMode';
|
||||
|
||||
suite('bracket matching', () => {
|
||||
class BracketMode extends MockMode {
|
||||
|
||||
private static readonly _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 = createTextModel('var x = (3 + (5-7)) + ((5+3)+5);', undefined, mode.getLanguageIdentifier());
|
||||
|
||||
withTestCodeEditor(null, { model: model }, (editor) => {
|
||||
let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, 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 = createTextModel('var x = (3 + (5-7)); y();', undefined, mode.getLanguageIdentifier());
|
||||
|
||||
withTestCodeEditor(null, { model: model }, (editor) => {
|
||||
let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, 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();
|
||||
});
|
||||
|
||||
test('Select to next bracket', () => {
|
||||
let mode = new BracketMode();
|
||||
let model = createTextModel('var x = (3 + (5-7)); y();', undefined, mode.getLanguageIdentifier());
|
||||
|
||||
withTestCodeEditor(null, { model: model }, (editor) => {
|
||||
let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController);
|
||||
|
||||
|
||||
// start position in open brackets
|
||||
editor.setPosition(new Position(1, 9));
|
||||
bracketMatchingController.selectToBracket(true);
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 20));
|
||||
assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20));
|
||||
|
||||
// start position in close brackets
|
||||
editor.setPosition(new Position(1, 20));
|
||||
bracketMatchingController.selectToBracket(true);
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 20));
|
||||
assert.deepEqual(editor.getSelection(), new Selection(1, 9, 1, 20));
|
||||
|
||||
// start position between brackets
|
||||
editor.setPosition(new Position(1, 16));
|
||||
bracketMatchingController.selectToBracket(true);
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 19));
|
||||
assert.deepEqual(editor.getSelection(), new Selection(1, 14, 1, 19));
|
||||
|
||||
// start position outside brackets
|
||||
editor.setPosition(new Position(1, 21));
|
||||
bracketMatchingController.selectToBracket(true);
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 25));
|
||||
assert.deepEqual(editor.getSelection(), new Selection(1, 23, 1, 25));
|
||||
|
||||
// do not break if no brackets are available
|
||||
editor.setPosition(new Position(1, 26));
|
||||
bracketMatchingController.selectToBracket(true);
|
||||
assert.deepEqual(editor.getPosition(), new Position(1, 26));
|
||||
assert.deepEqual(editor.getSelection(), new Selection(1, 26, 1, 26));
|
||||
|
||||
bracketMatchingController.dispose();
|
||||
});
|
||||
|
||||
model.dispose();
|
||||
mode.dispose();
|
||||
});
|
||||
|
||||
test('issue #1772: jump to enclosing brackets', () => {
|
||||
const text = [
|
||||
'const x = {',
|
||||
' something: [0, 1, 2],',
|
||||
' another: true,',
|
||||
' somethingmore: [0, 2, 4]',
|
||||
'};',
|
||||
].join('\n');
|
||||
const mode = new BracketMode();
|
||||
const model = createTextModel(text, undefined, mode.getLanguageIdentifier());
|
||||
|
||||
withTestCodeEditor(null, { model: model }, (editor) => {
|
||||
const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController);
|
||||
|
||||
editor.setPosition(new Position(3, 5));
|
||||
bracketMatchingController.jumpToBracket();
|
||||
assert.deepEqual(editor.getSelection(), new Selection(5, 1, 5, 1));
|
||||
|
||||
bracketMatchingController.dispose();
|
||||
});
|
||||
|
||||
model.dispose();
|
||||
mode.dispose();
|
||||
});
|
||||
|
||||
test('issue #43371: argument to not select brackets', () => {
|
||||
const text = [
|
||||
'const x = {',
|
||||
' something: [0, 1, 2],',
|
||||
' another: true,',
|
||||
' somethingmore: [0, 2, 4]',
|
||||
'};',
|
||||
].join('\n');
|
||||
const mode = new BracketMode();
|
||||
const model = createTextModel(text, undefined, mode.getLanguageIdentifier());
|
||||
|
||||
withTestCodeEditor(null, { model: model }, (editor) => {
|
||||
const bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController);
|
||||
|
||||
editor.setPosition(new Position(3, 5));
|
||||
bracketMatchingController.selectToBracket(false);
|
||||
assert.deepEqual(editor.getSelection(), new Selection(1, 12, 5, 1));
|
||||
|
||||
bracketMatchingController.dispose();
|
||||
});
|
||||
|
||||
model.dispose();
|
||||
mode.dispose();
|
||||
});
|
||||
|
||||
test('issue #45369: Select to Bracket with multicursor', () => {
|
||||
let mode = new BracketMode();
|
||||
let model = createTextModel('{ } { } { }', undefined, mode.getLanguageIdentifier());
|
||||
|
||||
withTestCodeEditor(null, { model: model }, (editor) => {
|
||||
let bracketMatchingController = editor.registerAndInstantiateContribution(BracketMatchingController.ID, BracketMatchingController);
|
||||
|
||||
// cursors inside brackets become selections of the entire bracket contents
|
||||
editor.setSelections([
|
||||
new Selection(1, 3, 1, 3),
|
||||
new Selection(1, 10, 1, 10),
|
||||
new Selection(1, 17, 1, 17)
|
||||
]);
|
||||
bracketMatchingController.selectToBracket(true);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(1, 8, 1, 13),
|
||||
new Selection(1, 16, 1, 19)
|
||||
]);
|
||||
|
||||
// cursors to the left of bracket pairs become selections of the entire pair
|
||||
editor.setSelections([
|
||||
new Selection(1, 1, 1, 1),
|
||||
new Selection(1, 6, 1, 6),
|
||||
new Selection(1, 14, 1, 14)
|
||||
]);
|
||||
bracketMatchingController.selectToBracket(true);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(1, 8, 1, 13),
|
||||
new Selection(1, 16, 1, 19)
|
||||
]);
|
||||
|
||||
// cursors just right of a bracket pair become selections of the entire pair
|
||||
editor.setSelections([
|
||||
new Selection(1, 5, 1, 5),
|
||||
new Selection(1, 13, 1, 13),
|
||||
new Selection(1, 19, 1, 19)
|
||||
]);
|
||||
bracketMatchingController.selectToBracket(true);
|
||||
assert.deepEqual(editor.getSelections(), [
|
||||
new Selection(1, 1, 1, 5),
|
||||
new Selection(1, 8, 1, 13),
|
||||
new Selection(1, 16, 1, 19)
|
||||
]);
|
||||
|
||||
bracketMatchingController.dispose();
|
||||
});
|
||||
|
||||
model.dispose();
|
||||
mode.dispose();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, IActionOptions, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICommand } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { MoveCaretCommand } from 'vs/editor/contrib/caretOperations/moveCaretCommand';
|
||||
|
||||
class MoveCaretAction extends EditorAction {
|
||||
|
||||
private readonly left: boolean;
|
||||
|
||||
constructor(left: boolean, opts: IActionOptions) {
|
||||
super(opts);
|
||||
|
||||
this.left = left;
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let commands: ICommand[] = [];
|
||||
let selections = editor.getSelections();
|
||||
|
||||
for (const selection of selections) {
|
||||
commands.push(new MoveCaretCommand(selection, this.left));
|
||||
}
|
||||
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, commands);
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
|
||||
class MoveCaretLeftAction extends MoveCaretAction {
|
||||
constructor() {
|
||||
super(true, {
|
||||
id: 'editor.action.moveCarretLeftAction',
|
||||
label: nls.localize('caret.moveLeft', "Move Selected Text Left"),
|
||||
alias: 'Move Selected Text Left',
|
||||
precondition: EditorContextKeys.writable
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class MoveCaretRightAction extends MoveCaretAction {
|
||||
constructor() {
|
||||
super(false, {
|
||||
id: 'editor.action.moveCarretRightAction',
|
||||
label: nls.localize('caret.moveRight', "Move Selected Text Right"),
|
||||
alias: 'Move Selected Text Right',
|
||||
precondition: EditorContextKeys.writable
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(MoveCaretLeftAction);
|
||||
registerEditorAction(MoveCaretRightAction);
|
||||
@@ -0,0 +1,55 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ICommand, ICursorStateComputerData, IEditOperationBuilder } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
|
||||
export class MoveCaretCommand implements ICommand {
|
||||
|
||||
private readonly _selection: Selection;
|
||||
private readonly _isMovingLeft: boolean;
|
||||
|
||||
constructor(selection: Selection, isMovingLeft: boolean) {
|
||||
this._selection = selection;
|
||||
this._isMovingLeft = isMovingLeft;
|
||||
}
|
||||
|
||||
public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
|
||||
if (this._selection.startLineNumber !== this._selection.endLineNumber || this._selection.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
const lineNumber = this._selection.startLineNumber;
|
||||
const startColumn = this._selection.startColumn;
|
||||
const endColumn = this._selection.endColumn;
|
||||
if (this._isMovingLeft && startColumn === 1) {
|
||||
return;
|
||||
}
|
||||
if (!this._isMovingLeft && endColumn === model.getLineMaxColumn(lineNumber)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._isMovingLeft) {
|
||||
const rangeBefore = new Range(lineNumber, startColumn - 1, lineNumber, startColumn);
|
||||
const charBefore = model.getValueInRange(rangeBefore);
|
||||
builder.addEditOperation(rangeBefore, null);
|
||||
builder.addEditOperation(new Range(lineNumber, endColumn, lineNumber, endColumn), charBefore);
|
||||
} else {
|
||||
const rangeAfter = new Range(lineNumber, endColumn, lineNumber, endColumn + 1);
|
||||
const charAfter = model.getValueInRange(rangeAfter);
|
||||
builder.addEditOperation(rangeAfter, null);
|
||||
builder.addEditOperation(new Range(lineNumber, startColumn, lineNumber, startColumn), charAfter);
|
||||
}
|
||||
}
|
||||
|
||||
public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
|
||||
if (this._isMovingLeft) {
|
||||
return new Selection(this._selection.startLineNumber, this._selection.startColumn - 1, this._selection.endLineNumber, this._selection.endColumn - 1);
|
||||
} else {
|
||||
return new Selection(this._selection.startLineNumber, this._selection.startColumn + 1, this._selection.endLineNumber, this._selection.endColumn + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { MoveCaretCommand } from 'vs/editor/contrib/caretOperations/moveCaretCommand';
|
||||
import { testCommand } from 'vs/editor/test/browser/testCommand';
|
||||
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { ReplaceCommand } from 'vs/editor/common/commands/replaceCommand';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ICommand } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { MoveOperations } from 'vs/editor/common/controller/cursorMoveOperations';
|
||||
|
||||
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.textInputFocus,
|
||||
primary: 0,
|
||||
mac: {
|
||||
primary: KeyMod.WinCtrl | KeyCode.KEY_T
|
||||
},
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = editor.getModel();
|
||||
let commands: ICommand[] = [];
|
||||
let selections = editor.getSelections();
|
||||
|
||||
for (let selection of selections) {
|
||||
if (!selection.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lineNumber = selection.startLineNumber;
|
||||
let column = selection.startColumn;
|
||||
|
||||
let lastColumn = model.getLineMaxColumn(lineNumber);
|
||||
|
||||
if (lineNumber === 1 && (column === 1 || (column === 2 && lastColumn === 2))) {
|
||||
// at beginning of file, nothing to do
|
||||
continue;
|
||||
}
|
||||
|
||||
// handle special case: when at end of line, transpose left two chars
|
||||
// otherwise, transpose left and right chars
|
||||
let endPosition = (column === lastColumn) ?
|
||||
selection.getPosition() :
|
||||
MoveOperations.rightPosition(model, selection.getPosition().lineNumber, selection.getPosition().column);
|
||||
|
||||
let middlePosition = MoveOperations.leftPosition(model, endPosition.lineNumber, endPosition.column);
|
||||
let beginPosition = MoveOperations.leftPosition(model, middlePosition.lineNumber, middlePosition.column);
|
||||
|
||||
let leftChar = model.getValueInRange(Range.fromPositions(beginPosition, middlePosition));
|
||||
let rightChar = model.getValueInRange(Range.fromPositions(middlePosition, endPosition));
|
||||
|
||||
let replaceRange = Range.fromPositions(beginPosition, endPosition);
|
||||
commands.push(new ReplaceCommand(replaceRange, rightChar + leftChar));
|
||||
}
|
||||
|
||||
if (commands.length > 0) {
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, commands);
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(TransposeLettersAction);
|
||||
244
lib/vscode/src/vs/editor/contrib/clipboard/clipboard.ts
Normal file
244
lib/vscode/src/vs/editor/contrib/clipboard/clipboard.ts
Normal file
@@ -0,0 +1,244 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { CopyOptions, InMemoryClipboardMetadataManager } from 'vs/editor/browser/controller/textAreaInput';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, registerEditorAction, Command, MultiCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { Handler } from 'vs/editor/common/editorCommon';
|
||||
|
||||
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.isEdge);
|
||||
// Firefox only supports navigator.clipboard.readText() in browser extensions.
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/readText#Browser_compatibility
|
||||
const supportsPaste = (browser.isFirefox ? document.queryCommandSupported('paste') : true);
|
||||
|
||||
function registerCommand<T extends Command>(command: T): T {
|
||||
command.register();
|
||||
return command;
|
||||
}
|
||||
|
||||
export const CutAction = supportsCut ? registerCommand(new MultiCommand({
|
||||
id: 'editor.action.clipboardCutAction',
|
||||
precondition: undefined,
|
||||
kbOpts: (
|
||||
// Do not bind cut keybindings in the browser,
|
||||
// since browsers do that for us and it avoids security prompts
|
||||
platform.isNative ? {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_X,
|
||||
win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_X, secondary: [KeyMod.Shift | KeyCode.Delete] },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
} : undefined
|
||||
),
|
||||
menuOpts: [{
|
||||
menuId: MenuId.MenubarEditMenu,
|
||||
group: '2_ccp',
|
||||
title: nls.localize({ key: 'miCut', comment: ['&& denotes a mnemonic'] }, "Cu&&t"),
|
||||
order: 1
|
||||
}, {
|
||||
menuId: MenuId.EditorContext,
|
||||
group: CLIPBOARD_CONTEXT_MENU_GROUP,
|
||||
title: nls.localize('actions.clipboard.cutLabel', "Cut"),
|
||||
when: EditorContextKeys.writable,
|
||||
order: 1,
|
||||
}, {
|
||||
menuId: MenuId.CommandPalette,
|
||||
group: '',
|
||||
title: nls.localize('actions.clipboard.cutLabel', "Cut"),
|
||||
order: 1
|
||||
}]
|
||||
})) : undefined;
|
||||
|
||||
export const CopyAction = supportsCopy ? registerCommand(new MultiCommand({
|
||||
id: 'editor.action.clipboardCopyAction',
|
||||
precondition: undefined,
|
||||
kbOpts: (
|
||||
// Do not bind copy keybindings in the browser,
|
||||
// since browsers do that for us and it avoids security prompts
|
||||
platform.isNative ? {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_C,
|
||||
win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_C, secondary: [KeyMod.CtrlCmd | KeyCode.Insert] },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
} : undefined
|
||||
),
|
||||
menuOpts: [{
|
||||
menuId: MenuId.MenubarEditMenu,
|
||||
group: '2_ccp',
|
||||
title: nls.localize({ key: 'miCopy', comment: ['&& denotes a mnemonic'] }, "&&Copy"),
|
||||
order: 2
|
||||
}, {
|
||||
menuId: MenuId.EditorContext,
|
||||
group: CLIPBOARD_CONTEXT_MENU_GROUP,
|
||||
title: nls.localize('actions.clipboard.copyLabel', "Copy"),
|
||||
order: 2,
|
||||
}, {
|
||||
menuId: MenuId.CommandPalette,
|
||||
group: '',
|
||||
title: nls.localize('actions.clipboard.copyLabel', "Copy"),
|
||||
order: 1
|
||||
}]
|
||||
})) : undefined;
|
||||
|
||||
export const PasteAction = supportsPaste ? registerCommand(new MultiCommand({
|
||||
id: 'editor.action.clipboardPasteAction',
|
||||
precondition: undefined,
|
||||
kbOpts: (
|
||||
// Do not bind paste keybindings in the browser,
|
||||
// since browsers do that for us and it avoids security prompts
|
||||
platform.isNative ? {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_V,
|
||||
win: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.Shift | KeyCode.Insert] },
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyCode.KEY_V, secondary: [KeyMod.Shift | KeyCode.Insert] },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
} : undefined
|
||||
),
|
||||
menuOpts: [{
|
||||
menuId: MenuId.MenubarEditMenu,
|
||||
group: '2_ccp',
|
||||
title: nls.localize({ key: 'miPaste', comment: ['&& denotes a mnemonic'] }, "&&Paste"),
|
||||
order: 3
|
||||
}, {
|
||||
menuId: MenuId.EditorContext,
|
||||
group: CLIPBOARD_CONTEXT_MENU_GROUP,
|
||||
title: nls.localize('actions.clipboard.pasteLabel', "Paste"),
|
||||
when: EditorContextKeys.writable,
|
||||
order: 3,
|
||||
}, {
|
||||
menuId: MenuId.CommandPalette,
|
||||
group: '',
|
||||
title: nls.localize('actions.clipboard.pasteLabel', "Paste"),
|
||||
order: 1
|
||||
}]
|
||||
})) : undefined;
|
||||
|
||||
class ExecCommandCopyWithSyntaxHighlightingAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.clipboardCopyWithSyntaxHighlightingAction',
|
||||
label: nls.localize('actions.clipboard.copyWithSyntaxHighlightingLabel', "Copy With Syntax Highlighting"),
|
||||
alias: 'Copy With Syntax Highlighting',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: 0,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emptySelectionClipboard = editor.getOption(EditorOption.emptySelectionClipboard);
|
||||
|
||||
if (!emptySelectionClipboard && editor.getSelection().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
CopyOptions.forceCopyWithSyntaxHighlighting = true;
|
||||
editor.focus();
|
||||
document.execCommand('copy');
|
||||
CopyOptions.forceCopyWithSyntaxHighlighting = false;
|
||||
}
|
||||
}
|
||||
|
||||
function registerExecCommandImpl(target: MultiCommand | undefined, browserCommand: 'cut' | 'copy'): void {
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. handle case when focus is in editor.
|
||||
target.addImplementation(10000, (accessor: ServicesAccessor, args: any) => {
|
||||
// Only if editor text focus (i.e. not if editor has widget focus).
|
||||
const focusedEditor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
|
||||
if (focusedEditor && focusedEditor.hasTextFocus()) {
|
||||
// Do not execute if there is no selection and empty selection clipboard is off
|
||||
const emptySelectionClipboard = focusedEditor.getOption(EditorOption.emptySelectionClipboard);
|
||||
const selection = focusedEditor.getSelection();
|
||||
if (selection && selection.isEmpty() && !emptySelectionClipboard) {
|
||||
return true;
|
||||
}
|
||||
document.execCommand(browserCommand);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 2. (default) handle case when focus is somewhere else.
|
||||
target.addImplementation(0, (accessor: ServicesAccessor, args: any) => {
|
||||
document.execCommand(browserCommand);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
registerExecCommandImpl(CutAction, 'cut');
|
||||
registerExecCommandImpl(CopyAction, 'copy');
|
||||
|
||||
if (PasteAction) {
|
||||
// 1. Paste: handle case when focus is in editor.
|
||||
PasteAction.addImplementation(10000, (accessor: ServicesAccessor, args: any) => {
|
||||
const codeEditorService = accessor.get(ICodeEditorService);
|
||||
const clipboardService = accessor.get(IClipboardService);
|
||||
|
||||
// Only if editor text focus (i.e. not if editor has widget focus).
|
||||
const focusedEditor = codeEditorService.getFocusedCodeEditor();
|
||||
if (focusedEditor && focusedEditor.hasTextFocus()) {
|
||||
const result = document.execCommand('paste');
|
||||
// Use the clipboard service if document.execCommand('paste') was not successful
|
||||
if (!result && platform.isWeb) {
|
||||
(async () => {
|
||||
const clipboardText = await clipboardService.readText();
|
||||
if (clipboardText !== '') {
|
||||
const metadata = InMemoryClipboardMetadataManager.INSTANCE.get(clipboardText);
|
||||
let pasteOnNewLine = false;
|
||||
let multicursorText: string[] | null = null;
|
||||
let mode: string | null = null;
|
||||
if (metadata) {
|
||||
pasteOnNewLine = (focusedEditor.getOption(EditorOption.emptySelectionClipboard) && !!metadata.isFromEmptySelection);
|
||||
multicursorText = (typeof metadata.multicursorText !== 'undefined' ? metadata.multicursorText : null);
|
||||
mode = metadata.mode;
|
||||
}
|
||||
focusedEditor.trigger('keyboard', Handler.Paste, {
|
||||
text: clipboardText,
|
||||
pasteOnNewLine,
|
||||
multicursorText,
|
||||
mode
|
||||
});
|
||||
}
|
||||
})();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// 2. Paste: (default) handle case when focus is somewhere else.
|
||||
PasteAction.addImplementation(0, (accessor: ServicesAccessor, args: any) => {
|
||||
document.execCommand('paste');
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (supportsCopyWithSyntaxHighlighting) {
|
||||
registerEditorAction(ExecCommandCopyWithSyntaxHighlightingAction);
|
||||
}
|
||||
267
lib/vscode/src/vs/editor/contrib/codeAction/codeAction.ts
Normal file
267
lib/vscode/src/vs/editor/contrib/codeAction/codeAction.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { equals, flatten, isNonEmptyArray, mergeSort, coalesce } from 'vs/base/common/arrays';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { illegalArgument, isPromiseCanceledError, onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState';
|
||||
import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { CodeActionFilter, CodeActionKind, CodeActionTrigger, filtersAction, mayIncludeActionsOfKind } from './types';
|
||||
import { IProgress, Progress } from 'vs/platform/progress/common/progress';
|
||||
|
||||
export const codeActionCommandId = 'editor.action.codeAction';
|
||||
export const refactorCommandId = 'editor.action.refactor';
|
||||
export const sourceActionCommandId = 'editor.action.sourceAction';
|
||||
export const organizeImportsCommandId = 'editor.action.organizeImports';
|
||||
export const fixAllCommandId = 'editor.action.fixAll';
|
||||
|
||||
export class CodeActionItem {
|
||||
|
||||
constructor(
|
||||
readonly action: modes.CodeAction,
|
||||
readonly provider: modes.CodeActionProvider | undefined,
|
||||
) { }
|
||||
|
||||
async resolve(token: CancellationToken): Promise<this> {
|
||||
if (this.provider?.resolveCodeAction && !this.action.edit) {
|
||||
let action: modes.CodeAction | undefined | null;
|
||||
try {
|
||||
action = await this.provider.resolveCodeAction(this.action, token);
|
||||
} catch (err) {
|
||||
onUnexpectedExternalError(err);
|
||||
}
|
||||
if (action) {
|
||||
this.action.edit = action.edit;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodeActionSet extends IDisposable {
|
||||
readonly validActions: readonly CodeActionItem[];
|
||||
readonly allActions: readonly CodeActionItem[];
|
||||
readonly hasAutoFix: boolean;
|
||||
|
||||
readonly documentation: readonly modes.Command[];
|
||||
}
|
||||
|
||||
class ManagedCodeActionSet extends Disposable implements CodeActionSet {
|
||||
|
||||
private static codeActionsComparator({ action: a }: CodeActionItem, { action: b }: CodeActionItem): number {
|
||||
if (a.isPreferred && !b.isPreferred) {
|
||||
return -1;
|
||||
} else if (!a.isPreferred && b.isPreferred) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (isNonEmptyArray(a.diagnostics)) {
|
||||
if (isNonEmptyArray(b.diagnostics)) {
|
||||
return a.diagnostics[0].message.localeCompare(b.diagnostics[0].message);
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
} else if (isNonEmptyArray(b.diagnostics)) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0; // both have no diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
public readonly validActions: readonly CodeActionItem[];
|
||||
public readonly allActions: readonly CodeActionItem[];
|
||||
|
||||
public constructor(
|
||||
actions: readonly CodeActionItem[],
|
||||
public readonly documentation: readonly modes.Command[],
|
||||
disposables: DisposableStore,
|
||||
) {
|
||||
super();
|
||||
this._register(disposables);
|
||||
this.allActions = mergeSort([...actions], ManagedCodeActionSet.codeActionsComparator);
|
||||
this.validActions = this.allActions.filter(({ action }) => !action.disabled);
|
||||
}
|
||||
|
||||
public get hasAutoFix() {
|
||||
return this.validActions.some(({ action: fix }) => !!fix.kind && CodeActionKind.QuickFix.contains(new CodeActionKind(fix.kind)) && !!fix.isPreferred);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const emptyCodeActionsResponse = { actions: [] as CodeActionItem[], documentation: undefined };
|
||||
|
||||
export function getCodeActions(
|
||||
model: ITextModel,
|
||||
rangeOrSelection: Range | Selection,
|
||||
trigger: CodeActionTrigger,
|
||||
progress: IProgress<modes.CodeActionProvider>,
|
||||
token: CancellationToken,
|
||||
): Promise<CodeActionSet> {
|
||||
const filter = trigger.filter || {};
|
||||
|
||||
const codeActionContext: modes.CodeActionContext = {
|
||||
only: filter.include?.value,
|
||||
trigger: trigger.type,
|
||||
};
|
||||
|
||||
const cts = new TextModelCancellationTokenSource(model, token);
|
||||
const providers = getCodeActionProviders(model, filter);
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
const promises = providers.map(async provider => {
|
||||
try {
|
||||
progress.report(provider);
|
||||
const providedCodeActions = await provider.provideCodeActions(model, rangeOrSelection, codeActionContext, cts.token);
|
||||
if (providedCodeActions) {
|
||||
disposables.add(providedCodeActions);
|
||||
}
|
||||
|
||||
if (cts.token.isCancellationRequested) {
|
||||
return emptyCodeActionsResponse;
|
||||
}
|
||||
|
||||
const filteredActions = (providedCodeActions?.actions || []).filter(action => action && filtersAction(filter, action));
|
||||
const documentation = getDocumentation(provider, filteredActions, filter.include);
|
||||
return {
|
||||
actions: filteredActions.map(action => new CodeActionItem(action, provider)),
|
||||
documentation
|
||||
};
|
||||
} catch (err) {
|
||||
if (isPromiseCanceledError(err)) {
|
||||
throw err;
|
||||
}
|
||||
onUnexpectedExternalError(err);
|
||||
return emptyCodeActionsResponse;
|
||||
}
|
||||
});
|
||||
|
||||
const listener = modes.CodeActionProviderRegistry.onDidChange(() => {
|
||||
const newProviders = modes.CodeActionProviderRegistry.all(model);
|
||||
if (!equals(newProviders, providers)) {
|
||||
cts.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(actions => {
|
||||
const allActions = flatten(actions.map(x => x.actions));
|
||||
const allDocumentation = coalesce(actions.map(x => x.documentation));
|
||||
return new ManagedCodeActionSet(allActions, allDocumentation, disposables);
|
||||
})
|
||||
.finally(() => {
|
||||
listener.dispose();
|
||||
cts.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
function getCodeActionProviders(
|
||||
model: ITextModel,
|
||||
filter: CodeActionFilter
|
||||
) {
|
||||
return modes.CodeActionProviderRegistry.all(model)
|
||||
// Don't include providers that we know will not return code actions of interest
|
||||
.filter(provider => {
|
||||
if (!provider.providedCodeActionKinds) {
|
||||
// We don't know what type of actions this provider will return.
|
||||
return true;
|
||||
}
|
||||
return provider.providedCodeActionKinds.some(kind => mayIncludeActionsOfKind(filter, new CodeActionKind(kind)));
|
||||
});
|
||||
}
|
||||
|
||||
function getDocumentation(
|
||||
provider: modes.CodeActionProvider,
|
||||
providedCodeActions: readonly modes.CodeAction[],
|
||||
only?: CodeActionKind
|
||||
): modes.Command | undefined {
|
||||
if (!provider.documentation) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const documentation = provider.documentation.map(entry => ({ kind: new CodeActionKind(entry.kind), command: entry.command }));
|
||||
|
||||
if (only) {
|
||||
let currentBest: { readonly kind: CodeActionKind, readonly command: modes.Command } | undefined;
|
||||
for (const entry of documentation) {
|
||||
if (entry.kind.contains(only)) {
|
||||
if (!currentBest) {
|
||||
currentBest = entry;
|
||||
} else {
|
||||
// Take best match
|
||||
if (currentBest.kind.contains(entry.kind)) {
|
||||
currentBest = entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (currentBest) {
|
||||
return currentBest?.command;
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, check to see if any of the provided actions match.
|
||||
for (const action of providedCodeActions) {
|
||||
if (!action.kind) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of documentation) {
|
||||
if (entry.kind.contains(new CodeActionKind(action.kind))) {
|
||||
return entry.command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
registerLanguageCommand('_executeCodeActionProvider', async function (accessor, args): Promise<ReadonlyArray<modes.CodeAction>> {
|
||||
const { resource, rangeOrSelection, kind, itemResolveCount } = args;
|
||||
if (!(resource instanceof URI)) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const validatedRangeOrSelection = Selection.isISelection(rangeOrSelection)
|
||||
? Selection.liftSelection(rangeOrSelection)
|
||||
: Range.isIRange(rangeOrSelection)
|
||||
? model.validateRange(rangeOrSelection)
|
||||
: undefined;
|
||||
|
||||
if (!validatedRangeOrSelection) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const codeActionSet = await getCodeActions(
|
||||
model,
|
||||
validatedRangeOrSelection,
|
||||
{ type: modes.CodeActionTriggerType.Manual, filter: { includeSourceActions: true, include: kind && kind.value ? new CodeActionKind(kind.value) : undefined } },
|
||||
Progress.None,
|
||||
CancellationToken.None);
|
||||
|
||||
|
||||
const resolving: Promise<any>[] = [];
|
||||
const resolveCount = Math.min(codeActionSet.validActions.length, typeof itemResolveCount === 'number' ? itemResolveCount : 0);
|
||||
for (let i = 0; i < resolveCount; i++) {
|
||||
resolving.push(codeActionSet.validActions[i].resolve(CancellationToken.None));
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(resolving);
|
||||
return codeActionSet.validActions.map(item => item.action);
|
||||
} finally {
|
||||
setTimeout(() => codeActionSet.dispose(), 100);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,442 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IAnchor } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Lazy } from 'vs/base/common/lazy';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { escapeRegExpCharacters } from 'vs/base/common/strings';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
import { IBulkEditService, ResourceEdit } from 'vs/editor/browser/services/bulkEditService';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { CodeActionTriggerType } from 'vs/editor/common/modes';
|
||||
import { codeActionCommandId, CodeActionItem, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/codeAction';
|
||||
import { CodeActionUi } from 'vs/editor/contrib/codeAction/codeActionUi';
|
||||
import { MessageController } from 'vs/editor/contrib/message/messageController';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { IMarkerService } from 'vs/platform/markers/common/markers';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IEditorProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { CodeActionModel, CodeActionsState, SUPPORTED_CODE_ACTIONS } from './codeActionModel';
|
||||
import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionFilter, CodeActionKind, CodeActionTrigger } from './types';
|
||||
|
||||
function contextKeyForSupportedActions(kind: CodeActionKind) {
|
||||
return ContextKeyExpr.regex(
|
||||
SUPPORTED_CODE_ACTIONS.keys()[0],
|
||||
new RegExp('(\\s|^)' + escapeRegExpCharacters(kind.value) + '\\b'));
|
||||
}
|
||||
|
||||
const argsSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
defaultSnippets: [{ body: { kind: '' } }],
|
||||
properties: {
|
||||
'kind': {
|
||||
type: 'string',
|
||||
description: nls.localize('args.schema.kind', "Kind of the code action to run."),
|
||||
},
|
||||
'apply': {
|
||||
type: 'string',
|
||||
description: nls.localize('args.schema.apply', "Controls when the returned actions are applied."),
|
||||
default: CodeActionAutoApply.IfSingle,
|
||||
enum: [CodeActionAutoApply.First, CodeActionAutoApply.IfSingle, CodeActionAutoApply.Never],
|
||||
enumDescriptions: [
|
||||
nls.localize('args.schema.apply.first', "Always apply the first returned code action."),
|
||||
nls.localize('args.schema.apply.ifSingle', "Apply the first returned code action if it is the only one."),
|
||||
nls.localize('args.schema.apply.never', "Do not apply the returned code actions."),
|
||||
]
|
||||
},
|
||||
'preferred': {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: nls.localize('args.schema.preferred', "Controls if only preferred code actions should be returned."),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export class QuickFixController extends Disposable implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.quickFixController';
|
||||
|
||||
public static get(editor: ICodeEditor): QuickFixController {
|
||||
return editor.getContribution<QuickFixController>(QuickFixController.ID);
|
||||
}
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
private readonly _model: CodeActionModel;
|
||||
private readonly _ui: Lazy<CodeActionUi>;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IMarkerService markerService: IMarkerService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IEditorProgressService progressService: IEditorProgressService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._editor = editor;
|
||||
this._model = this._register(new CodeActionModel(this._editor, markerService, contextKeyService, progressService));
|
||||
this._register(this._model.onDidChangeState(newState => this.update(newState)));
|
||||
|
||||
this._ui = new Lazy(() =>
|
||||
this._register(new CodeActionUi(editor, QuickFixAction.Id, AutoFixAction.Id, {
|
||||
applyCodeAction: async (action, retrigger) => {
|
||||
try {
|
||||
await this._applyCodeAction(action);
|
||||
} finally {
|
||||
if (retrigger) {
|
||||
this._trigger({ type: CodeActionTriggerType.Auto, filter: {} });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, this._instantiationService))
|
||||
);
|
||||
}
|
||||
|
||||
private update(newState: CodeActionsState.State): void {
|
||||
this._ui.getValue().update(newState);
|
||||
}
|
||||
|
||||
public showCodeActions(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition) {
|
||||
return this._ui.getValue().showCodeActionList(trigger, actions, at, { includeDisabledActions: false });
|
||||
}
|
||||
|
||||
public manualTriggerAtCurrentPosition(
|
||||
notAvailableMessage: string,
|
||||
filter?: CodeActionFilter,
|
||||
autoApply?: CodeActionAutoApply
|
||||
): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
MessageController.get(this._editor).closeMessage();
|
||||
const triggerPosition = this._editor.getPosition();
|
||||
this._trigger({ type: CodeActionTriggerType.Manual, filter, autoApply, context: { notAvailableMessage, position: triggerPosition } });
|
||||
}
|
||||
|
||||
private _trigger(trigger: CodeActionTrigger) {
|
||||
return this._model.trigger(trigger);
|
||||
}
|
||||
|
||||
private _applyCodeAction(action: CodeActionItem): Promise<void> {
|
||||
return this._instantiationService.invokeFunction(applyCodeAction, action, this._editor);
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyCodeAction(
|
||||
accessor: ServicesAccessor,
|
||||
item: CodeActionItem,
|
||||
editor?: ICodeEditor,
|
||||
): Promise<void> {
|
||||
const bulkEditService = accessor.get(IBulkEditService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const telemetryService = accessor.get(ITelemetryService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
|
||||
type ApplyCodeActionEvent = {
|
||||
codeActionTitle: string;
|
||||
codeActionKind: string | undefined;
|
||||
codeActionIsPreferred: boolean;
|
||||
};
|
||||
type ApplyCodeEventClassification = {
|
||||
codeActionTitle: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
codeActionKind: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
codeActionIsPreferred: { classification: 'SystemMetaData', purpose: 'FeatureInsight' };
|
||||
};
|
||||
|
||||
telemetryService.publicLog2<ApplyCodeActionEvent, ApplyCodeEventClassification>('codeAction.applyCodeAction', {
|
||||
codeActionTitle: item.action.title,
|
||||
codeActionKind: item.action.kind,
|
||||
codeActionIsPreferred: !!item.action.isPreferred,
|
||||
});
|
||||
|
||||
await item.resolve(CancellationToken.None);
|
||||
|
||||
if (item.action.edit) {
|
||||
await bulkEditService.apply(ResourceEdit.convert(item.action.edit), { editor, label: item.action.title });
|
||||
}
|
||||
|
||||
if (item.action.command) {
|
||||
try {
|
||||
await commandService.executeCommand(item.action.command.id, ...(item.action.command.arguments || []));
|
||||
} catch (err) {
|
||||
const message = asMessage(err);
|
||||
notificationService.error(
|
||||
typeof message === 'string'
|
||||
? message
|
||||
: nls.localize('applyCodeActionFailed', "An unknown error occurred while applying the code action"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function asMessage(err: any): string | undefined {
|
||||
if (typeof err === 'string') {
|
||||
return err;
|
||||
} else if (err instanceof Error && typeof err.message === 'string') {
|
||||
return err.message;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function triggerCodeActionsForEditorSelection(
|
||||
editor: ICodeEditor,
|
||||
notAvailableMessage: string,
|
||||
filter: CodeActionFilter | undefined,
|
||||
autoApply: CodeActionAutoApply | undefined
|
||||
): void {
|
||||
if (editor.hasModel()) {
|
||||
const controller = QuickFixController.get(editor);
|
||||
if (controller) {
|
||||
controller.manualTriggerAtCurrentPosition(notAvailableMessage, filter, autoApply);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class QuickFixAction extends EditorAction {
|
||||
|
||||
static readonly Id = 'editor.action.quickFix';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: QuickFixAction.Id,
|
||||
label: nls.localize('quickfix.trigger.label', "Quick Fix..."),
|
||||
alias: 'Quick Fix...',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.US_DOT,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
return triggerCodeActionsForEditorSelection(editor, nls.localize('editor.action.quickFix.noneMessage', "No code actions available"), undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export class CodeActionCommand extends EditorCommand {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: codeActionCommandId,
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider),
|
||||
description: {
|
||||
description: 'Trigger a code action',
|
||||
args: [{ name: 'args', schema: argsSchema, }]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, userArgs: any) {
|
||||
const args = CodeActionCommandArgs.fromUser(userArgs, {
|
||||
kind: CodeActionKind.Empty,
|
||||
apply: CodeActionAutoApply.IfSingle,
|
||||
});
|
||||
return triggerCodeActionsForEditorSelection(editor,
|
||||
typeof userArgs?.kind === 'string'
|
||||
? args.preferred
|
||||
? nls.localize('editor.action.codeAction.noneMessage.preferred.kind', "No preferred code actions for '{0}' available", userArgs.kind)
|
||||
: nls.localize('editor.action.codeAction.noneMessage.kind', "No code actions for '{0}' available", userArgs.kind)
|
||||
: args.preferred
|
||||
? nls.localize('editor.action.codeAction.noneMessage.preferred', "No preferred code actions available")
|
||||
: nls.localize('editor.action.codeAction.noneMessage', "No code actions available"),
|
||||
{
|
||||
include: args.kind,
|
||||
includeSourceActions: true,
|
||||
onlyIncludePreferredActions: args.preferred,
|
||||
},
|
||||
args.apply);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class RefactorAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: refactorCommandId,
|
||||
label: nls.localize('refactor.label', "Refactor..."),
|
||||
alias: 'Refactor...',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_R,
|
||||
mac: {
|
||||
primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_R
|
||||
},
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
contextMenuOpts: {
|
||||
group: '1_modification',
|
||||
order: 2,
|
||||
when: ContextKeyExpr.and(
|
||||
EditorContextKeys.writable,
|
||||
contextKeyForSupportedActions(CodeActionKind.Refactor)),
|
||||
},
|
||||
description: {
|
||||
description: 'Refactor...',
|
||||
args: [{ name: 'args', schema: argsSchema }]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(_accessor: ServicesAccessor, editor: ICodeEditor, userArgs: any): void {
|
||||
const args = CodeActionCommandArgs.fromUser(userArgs, {
|
||||
kind: CodeActionKind.Refactor,
|
||||
apply: CodeActionAutoApply.Never
|
||||
});
|
||||
return triggerCodeActionsForEditorSelection(editor,
|
||||
typeof userArgs?.kind === 'string'
|
||||
? args.preferred
|
||||
? nls.localize('editor.action.refactor.noneMessage.preferred.kind', "No preferred refactorings for '{0}' available", userArgs.kind)
|
||||
: nls.localize('editor.action.refactor.noneMessage.kind', "No refactorings for '{0}' available", userArgs.kind)
|
||||
: args.preferred
|
||||
? nls.localize('editor.action.refactor.noneMessage.preferred', "No preferred refactorings available")
|
||||
: nls.localize('editor.action.refactor.noneMessage', "No refactorings available"),
|
||||
{
|
||||
include: CodeActionKind.Refactor.contains(args.kind) ? args.kind : CodeActionKind.None,
|
||||
onlyIncludePreferredActions: args.preferred,
|
||||
},
|
||||
args.apply);
|
||||
}
|
||||
}
|
||||
|
||||
export class SourceAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: sourceActionCommandId,
|
||||
label: nls.localize('source.label', "Source Action..."),
|
||||
alias: 'Source Action...',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCodeActionsProvider),
|
||||
contextMenuOpts: {
|
||||
group: '1_modification',
|
||||
order: 2.1,
|
||||
when: ContextKeyExpr.and(
|
||||
EditorContextKeys.writable,
|
||||
contextKeyForSupportedActions(CodeActionKind.Source)),
|
||||
},
|
||||
description: {
|
||||
description: 'Source Action...',
|
||||
args: [{ name: 'args', schema: argsSchema }]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(_accessor: ServicesAccessor, editor: ICodeEditor, userArgs: any): void {
|
||||
const args = CodeActionCommandArgs.fromUser(userArgs, {
|
||||
kind: CodeActionKind.Source,
|
||||
apply: CodeActionAutoApply.Never
|
||||
});
|
||||
return triggerCodeActionsForEditorSelection(editor,
|
||||
typeof userArgs?.kind === 'string'
|
||||
? args.preferred
|
||||
? nls.localize('editor.action.source.noneMessage.preferred.kind', "No preferred source actions for '{0}' available", userArgs.kind)
|
||||
: nls.localize('editor.action.source.noneMessage.kind', "No source actions for '{0}' available", userArgs.kind)
|
||||
: args.preferred
|
||||
? nls.localize('editor.action.source.noneMessage.preferred', "No preferred source actions available")
|
||||
: nls.localize('editor.action.source.noneMessage', "No source actions available"),
|
||||
{
|
||||
include: CodeActionKind.Source.contains(args.kind) ? args.kind : CodeActionKind.None,
|
||||
includeSourceActions: true,
|
||||
onlyIncludePreferredActions: args.preferred,
|
||||
},
|
||||
args.apply);
|
||||
}
|
||||
}
|
||||
|
||||
export class OrganizeImportsAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: organizeImportsCommandId,
|
||||
label: nls.localize('organizeImports.label', "Organize Imports"),
|
||||
alias: 'Organize Imports',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.writable,
|
||||
contextKeyForSupportedActions(CodeActionKind.SourceOrganizeImports)),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_O,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
return triggerCodeActionsForEditorSelection(editor,
|
||||
nls.localize('editor.action.organize.noneMessage', "No organize imports action available"),
|
||||
{ include: CodeActionKind.SourceOrganizeImports, includeSourceActions: true },
|
||||
CodeActionAutoApply.IfSingle);
|
||||
}
|
||||
}
|
||||
|
||||
export class FixAllAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: fixAllCommandId,
|
||||
label: nls.localize('fixAll.label', "Fix All"),
|
||||
alias: 'Fix All',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.writable,
|
||||
contextKeyForSupportedActions(CodeActionKind.SourceFixAll))
|
||||
});
|
||||
}
|
||||
|
||||
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
return triggerCodeActionsForEditorSelection(editor,
|
||||
nls.localize('fixAll.noneMessage', "No fix all action available"),
|
||||
{ include: CodeActionKind.SourceFixAll, includeSourceActions: true },
|
||||
CodeActionAutoApply.IfSingle);
|
||||
}
|
||||
}
|
||||
|
||||
export class AutoFixAction extends EditorAction {
|
||||
|
||||
static readonly Id = 'editor.action.autoFix';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: AutoFixAction.Id,
|
||||
label: nls.localize('autoFix.label', "Auto Fix..."),
|
||||
alias: 'Auto Fix...',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.writable,
|
||||
contextKeyForSupportedActions(CodeActionKind.QuickFix)),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.US_DOT,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_DOT
|
||||
},
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(_accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
return triggerCodeActionsForEditorSelection(editor,
|
||||
nls.localize('editor.action.autoFix.noneMessage', "No auto fixes available"),
|
||||
{
|
||||
include: CodeActionKind.QuickFix,
|
||||
onlyIncludePreferredActions: true
|
||||
},
|
||||
CodeActionAutoApply.IfSingle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { CodeActionCommand, OrganizeImportsAction, QuickFixAction, QuickFixController, RefactorAction, SourceAction, AutoFixAction, FixAllAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
|
||||
|
||||
|
||||
registerEditorContribution(QuickFixController.ID, QuickFixController);
|
||||
registerEditorAction(QuickFixAction);
|
||||
registerEditorAction(RefactorAction);
|
||||
registerEditorAction(SourceAction);
|
||||
registerEditorAction(OrganizeImportsAction);
|
||||
registerEditorAction(AutoFixAction);
|
||||
registerEditorAction(FixAllAction);
|
||||
registerEditorCommand(new CodeActionCommand());
|
||||
226
lib/vscode/src/vs/editor/contrib/codeAction/codeActionMenu.ts
Normal file
226
lib/vscode/src/vs/editor/contrib/codeAction/codeActionMenu.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { getDomNodePagePosition } from 'vs/base/browser/dom';
|
||||
import { IAnchor } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { Action, IAction, Separator } from 'vs/base/common/actions';
|
||||
import { canceled } from 'vs/base/common/errors';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { Lazy } from 'vs/base/common/lazy';
|
||||
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IPosition, Position } from 'vs/editor/common/core/position';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { CodeAction, CodeActionProviderRegistry, Command } from 'vs/editor/common/modes';
|
||||
import { codeActionCommandId, CodeActionItem, CodeActionSet, fixAllCommandId, organizeImportsCommandId, refactorCommandId, sourceActionCommandId } from 'vs/editor/contrib/codeAction/codeAction';
|
||||
import { CodeActionAutoApply, CodeActionCommandArgs, CodeActionTrigger, CodeActionKind } from 'vs/editor/contrib/codeAction/types';
|
||||
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
|
||||
|
||||
interface CodeActionWidgetDelegate {
|
||||
onSelectCodeAction: (action: CodeActionItem) => Promise<any>;
|
||||
}
|
||||
|
||||
interface ResolveCodeActionKeybinding {
|
||||
readonly kind: CodeActionKind;
|
||||
readonly preferred: boolean;
|
||||
readonly resolvedKeybinding: ResolvedKeybinding;
|
||||
}
|
||||
|
||||
class CodeActionAction extends Action {
|
||||
constructor(
|
||||
public readonly action: CodeAction,
|
||||
callback: () => Promise<void>,
|
||||
) {
|
||||
super(action.command ? action.command.id : action.title, action.title, undefined, !action.disabled, callback);
|
||||
}
|
||||
}
|
||||
|
||||
export interface CodeActionShowOptions {
|
||||
readonly includeDisabledActions: boolean;
|
||||
}
|
||||
|
||||
export class CodeActionMenu extends Disposable {
|
||||
|
||||
private _visible: boolean = false;
|
||||
private readonly _showingActions = this._register(new MutableDisposable<CodeActionSet>());
|
||||
|
||||
private readonly _keybindingResolver: CodeActionKeybindingResolver;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
private readonly _delegate: CodeActionWidgetDelegate,
|
||||
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
|
||||
@IKeybindingService keybindingService: IKeybindingService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._keybindingResolver = new CodeActionKeybindingResolver({
|
||||
getKeybindings: () => keybindingService.getKeybindings()
|
||||
});
|
||||
}
|
||||
|
||||
get isVisible(): boolean {
|
||||
return this._visible;
|
||||
}
|
||||
|
||||
public async show(trigger: CodeActionTrigger, codeActions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise<void> {
|
||||
const actionsToShow = options.includeDisabledActions ? codeActions.allActions : codeActions.validActions;
|
||||
if (!actionsToShow.length) {
|
||||
this._visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._editor.getDomNode()) {
|
||||
// cancel when editor went off-dom
|
||||
this._visible = false;
|
||||
throw canceled();
|
||||
}
|
||||
|
||||
this._visible = true;
|
||||
this._showingActions.value = codeActions;
|
||||
|
||||
const menuActions = this.getMenuActions(trigger, actionsToShow, codeActions.documentation);
|
||||
|
||||
const anchor = Position.isIPosition(at) ? this._toCoords(at) : at || { x: 0, y: 0 };
|
||||
const resolver = this._keybindingResolver.getResolver();
|
||||
|
||||
this._contextMenuService.showContextMenu({
|
||||
domForShadowRoot: this._editor.getDomNode()!,
|
||||
getAnchor: () => anchor,
|
||||
getActions: () => menuActions,
|
||||
onHide: () => {
|
||||
this._visible = false;
|
||||
this._editor.focus();
|
||||
},
|
||||
autoSelectFirstItem: true,
|
||||
getKeyBinding: action => action instanceof CodeActionAction ? resolver(action.action) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private getMenuActions(
|
||||
trigger: CodeActionTrigger,
|
||||
actionsToShow: readonly CodeActionItem[],
|
||||
documentation: readonly Command[]
|
||||
): IAction[] {
|
||||
const toCodeActionAction = (item: CodeActionItem): CodeActionAction => new CodeActionAction(item.action, () => this._delegate.onSelectCodeAction(item));
|
||||
|
||||
const result: IAction[] = actionsToShow
|
||||
.map(toCodeActionAction);
|
||||
|
||||
const allDocumentation: Command[] = [...documentation];
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (model && result.length) {
|
||||
for (const provider of CodeActionProviderRegistry.all(model)) {
|
||||
if (provider._getAdditionalMenuItems) {
|
||||
allDocumentation.push(...provider._getAdditionalMenuItems({ trigger: trigger.type, only: trigger.filter?.include?.value }, actionsToShow.map(item => item.action)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allDocumentation.length) {
|
||||
result.push(new Separator(), ...allDocumentation.map(command => toCodeActionAction(new CodeActionItem({
|
||||
title: command.title,
|
||||
command: command,
|
||||
}, undefined))));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _toCoords(position: IPosition): { x: number, y: number } {
|
||||
if (!this._editor.hasModel()) {
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
this._editor.revealPosition(position, ScrollType.Immediate);
|
||||
this._editor.render();
|
||||
|
||||
// Translate to absolute editor position
|
||||
const cursorCoords = this._editor.getScrolledVisiblePosition(position);
|
||||
const editorCoords = getDomNodePagePosition(this._editor.getDomNode());
|
||||
const x = editorCoords.left + cursorCoords.left;
|
||||
const y = editorCoords.top + cursorCoords.top + cursorCoords.height;
|
||||
|
||||
return { x, y };
|
||||
}
|
||||
}
|
||||
|
||||
export class CodeActionKeybindingResolver {
|
||||
private static readonly codeActionCommands: readonly string[] = [
|
||||
refactorCommandId,
|
||||
codeActionCommandId,
|
||||
sourceActionCommandId,
|
||||
organizeImportsCommandId,
|
||||
fixAllCommandId
|
||||
];
|
||||
|
||||
constructor(
|
||||
private readonly _keybindingProvider: {
|
||||
getKeybindings(): readonly ResolvedKeybindingItem[],
|
||||
},
|
||||
) { }
|
||||
|
||||
public getResolver(): (action: CodeAction) => ResolvedKeybinding | undefined {
|
||||
// Lazy since we may not actually ever read the value
|
||||
const allCodeActionBindings = new Lazy<readonly ResolveCodeActionKeybinding[]>(() =>
|
||||
this._keybindingProvider.getKeybindings()
|
||||
.filter(item => CodeActionKeybindingResolver.codeActionCommands.indexOf(item.command!) >= 0)
|
||||
.filter(item => item.resolvedKeybinding)
|
||||
.map((item): ResolveCodeActionKeybinding => {
|
||||
// Special case these commands since they come built-in with VS Code and don't use 'commandArgs'
|
||||
let commandArgs = item.commandArgs;
|
||||
if (item.command === organizeImportsCommandId) {
|
||||
commandArgs = { kind: CodeActionKind.SourceOrganizeImports.value };
|
||||
} else if (item.command === fixAllCommandId) {
|
||||
commandArgs = { kind: CodeActionKind.SourceFixAll.value };
|
||||
}
|
||||
|
||||
return {
|
||||
resolvedKeybinding: item.resolvedKeybinding!,
|
||||
...CodeActionCommandArgs.fromUser(commandArgs, {
|
||||
kind: CodeActionKind.None,
|
||||
apply: CodeActionAutoApply.Never
|
||||
})
|
||||
};
|
||||
}));
|
||||
|
||||
return (action) => {
|
||||
if (action.kind) {
|
||||
const binding = this.bestKeybindingForCodeAction(action, allCodeActionBindings.getValue());
|
||||
return binding?.resolvedKeybinding;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
}
|
||||
|
||||
private bestKeybindingForCodeAction(
|
||||
action: CodeAction,
|
||||
candidates: readonly ResolveCodeActionKeybinding[],
|
||||
): ResolveCodeActionKeybinding | undefined {
|
||||
if (!action.kind) {
|
||||
return undefined;
|
||||
}
|
||||
const kind = new CodeActionKind(action.kind);
|
||||
|
||||
return candidates
|
||||
.filter(candidate => candidate.kind.contains(kind))
|
||||
.filter(candidate => {
|
||||
if (candidate.preferred) {
|
||||
// If the candidate keybinding only applies to preferred actions, the this action must also be preferred
|
||||
return action.isPreferred;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.reduceRight((currentBest, candidate) => {
|
||||
if (!currentBest) {
|
||||
return candidate;
|
||||
}
|
||||
// Select the more specific binding
|
||||
return currentBest.kind.contains(candidate.kind) ? candidate : currentBest;
|
||||
}, undefined as ResolveCodeActionKeybinding | undefined);
|
||||
}
|
||||
}
|
||||
252
lib/vscode/src/vs/editor/contrib/codeAction/codeActionModel.ts
Normal file
252
lib/vscode/src/vs/editor/contrib/codeAction/codeActionModel.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancelablePromise, createCancelablePromise, TimeoutTimer } from 'vs/base/common/async';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
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 { CodeActionProviderRegistry, CodeActionTriggerType } from 'vs/editor/common/modes';
|
||||
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IMarkerService } from 'vs/platform/markers/common/markers';
|
||||
import { IEditorProgressService, Progress } from 'vs/platform/progress/common/progress';
|
||||
import { getCodeActions, CodeActionSet } from './codeAction';
|
||||
import { CodeActionTrigger } from './types';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
export const SUPPORTED_CODE_ACTIONS = new RawContextKey<string>('supportedCodeAction', '');
|
||||
|
||||
export type TriggeredCodeAction = undefined | {
|
||||
readonly selection: Selection;
|
||||
readonly trigger: CodeActionTrigger;
|
||||
readonly position: Position;
|
||||
};
|
||||
|
||||
class CodeActionOracle extends Disposable {
|
||||
|
||||
private readonly _autoTriggerTimer = this._register(new TimeoutTimer());
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
private readonly _markerService: IMarkerService,
|
||||
private readonly _signalChange: (triggered: TriggeredCodeAction) => void,
|
||||
private readonly _delay: number = 250,
|
||||
) {
|
||||
super();
|
||||
this._register(this._markerService.onMarkerChanged(e => this._onMarkerChanges(e)));
|
||||
this._register(this._editor.onDidChangeCursorPosition(() => this._onCursorChange()));
|
||||
}
|
||||
|
||||
public trigger(trigger: CodeActionTrigger): TriggeredCodeAction {
|
||||
const selection = this._getRangeOfSelectionUnlessWhitespaceEnclosed(trigger);
|
||||
return this._createEventAndSignalChange(trigger, selection);
|
||||
}
|
||||
|
||||
private _onMarkerChanges(resources: readonly URI[]): void {
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resources.some(resource => isEqual(resource, model.uri))) {
|
||||
this._autoTriggerTimer.cancelAndSet(() => {
|
||||
this.trigger({ type: CodeActionTriggerType.Auto });
|
||||
}, this._delay);
|
||||
}
|
||||
}
|
||||
|
||||
private _onCursorChange(): void {
|
||||
this._autoTriggerTimer.cancelAndSet(() => {
|
||||
this.trigger({ type: CodeActionTriggerType.Auto });
|
||||
}, this._delay);
|
||||
}
|
||||
|
||||
private _getRangeOfMarker(selection: Selection): Range | undefined {
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
for (const marker of this._markerService.read({ resource: model.uri })) {
|
||||
const markerRange = model.validateRange(marker);
|
||||
if (Range.intersectRanges(markerRange, selection)) {
|
||||
return Range.lift(markerRange);
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getRangeOfSelectionUnlessWhitespaceEnclosed(trigger: CodeActionTrigger): Selection | undefined {
|
||||
if (!this._editor.hasModel()) {
|
||||
return undefined;
|
||||
}
|
||||
const model = this._editor.getModel();
|
||||
const selection = this._editor.getSelection();
|
||||
if (selection.isEmpty() && trigger.type === CodeActionTriggerType.Auto) {
|
||||
const { lineNumber, column } = selection.getPosition();
|
||||
const line = model.getLineContent(lineNumber);
|
||||
if (line.length === 0) {
|
||||
// empty line
|
||||
return undefined;
|
||||
} else if (column === 1) {
|
||||
// look only right
|
||||
if (/\s/.test(line[0])) {
|
||||
return undefined;
|
||||
}
|
||||
} else if (column === model.getLineMaxColumn(lineNumber)) {
|
||||
// look only left
|
||||
if (/\s/.test(line[line.length - 1])) {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
// look left and right
|
||||
if (/\s/.test(line[column - 2]) && /\s/.test(line[column - 1])) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
return selection;
|
||||
}
|
||||
|
||||
private _createEventAndSignalChange(trigger: CodeActionTrigger, selection: Selection | undefined): TriggeredCodeAction {
|
||||
const model = this._editor.getModel();
|
||||
if (!selection || !model) {
|
||||
// cancel
|
||||
this._signalChange(undefined);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const markerRange = this._getRangeOfMarker(selection);
|
||||
const position = markerRange ? markerRange.getStartPosition() : selection.getStartPosition();
|
||||
|
||||
const e: TriggeredCodeAction = {
|
||||
trigger,
|
||||
selection,
|
||||
position
|
||||
};
|
||||
this._signalChange(e);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
export namespace CodeActionsState {
|
||||
|
||||
export const enum Type {
|
||||
Empty,
|
||||
Triggered,
|
||||
}
|
||||
|
||||
export const Empty = { type: Type.Empty } as const;
|
||||
|
||||
export class Triggered {
|
||||
readonly type = Type.Triggered;
|
||||
|
||||
constructor(
|
||||
public readonly trigger: CodeActionTrigger,
|
||||
public readonly rangeOrSelection: Range | Selection,
|
||||
public readonly position: Position,
|
||||
public readonly actions: CancelablePromise<CodeActionSet>,
|
||||
) { }
|
||||
}
|
||||
|
||||
export type State = typeof Empty | Triggered;
|
||||
}
|
||||
|
||||
export class CodeActionModel extends Disposable {
|
||||
|
||||
private readonly _codeActionOracle = this._register(new MutableDisposable<CodeActionOracle>());
|
||||
private _state: CodeActionsState.State = CodeActionsState.Empty;
|
||||
private readonly _supportedCodeActions: IContextKey<string>;
|
||||
|
||||
private readonly _onDidChangeState = this._register(new Emitter<CodeActionsState.State>());
|
||||
public readonly onDidChangeState = this._onDidChangeState.event;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
private readonly _markerService: IMarkerService,
|
||||
contextKeyService: IContextKeyService,
|
||||
private readonly _progressService?: IEditorProgressService
|
||||
) {
|
||||
super();
|
||||
this._supportedCodeActions = SUPPORTED_CODE_ACTIONS.bindTo(contextKeyService);
|
||||
|
||||
this._register(this._editor.onDidChangeModel(() => this._update()));
|
||||
this._register(this._editor.onDidChangeModelLanguage(() => this._update()));
|
||||
this._register(CodeActionProviderRegistry.onDidChange(() => this._update()));
|
||||
|
||||
this._update();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this.setState(CodeActionsState.Empty, true);
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
this._codeActionOracle.value = undefined;
|
||||
|
||||
this.setState(CodeActionsState.Empty);
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (model
|
||||
&& CodeActionProviderRegistry.has(model)
|
||||
&& !this._editor.getOption(EditorOption.readOnly)
|
||||
) {
|
||||
const supportedActions: string[] = [];
|
||||
for (const provider of CodeActionProviderRegistry.all(model)) {
|
||||
if (Array.isArray(provider.providedCodeActionKinds)) {
|
||||
supportedActions.push(...provider.providedCodeActionKinds);
|
||||
}
|
||||
}
|
||||
|
||||
this._supportedCodeActions.set(supportedActions.join(' '));
|
||||
|
||||
this._codeActionOracle.value = new CodeActionOracle(this._editor, this._markerService, trigger => {
|
||||
if (!trigger) {
|
||||
this.setState(CodeActionsState.Empty);
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = createCancelablePromise(token => getCodeActions(model, trigger.selection, trigger.trigger, Progress.None, token));
|
||||
if (trigger.trigger.type === CodeActionTriggerType.Manual) {
|
||||
this._progressService?.showWhile(actions, 250);
|
||||
}
|
||||
|
||||
this.setState(new CodeActionsState.Triggered(trigger.trigger, trigger.selection, trigger.position, actions));
|
||||
|
||||
}, undefined);
|
||||
this._codeActionOracle.value.trigger({ type: CodeActionTriggerType.Auto });
|
||||
} else {
|
||||
this._supportedCodeActions.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public trigger(trigger: CodeActionTrigger) {
|
||||
if (this._codeActionOracle.value) {
|
||||
this._codeActionOracle.value.trigger(trigger);
|
||||
}
|
||||
}
|
||||
|
||||
private setState(newState: CodeActionsState.State, skipNotify?: boolean) {
|
||||
if (newState === this._state) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel old request
|
||||
if (this._state.type === CodeActionsState.Type.Triggered) {
|
||||
this._state.actions.cancel();
|
||||
}
|
||||
|
||||
this._state = newState;
|
||||
|
||||
if (!skipNotify) {
|
||||
this._onDidChangeState.fire(newState);
|
||||
}
|
||||
}
|
||||
}
|
||||
148
lib/vscode/src/vs/editor/contrib/codeAction/codeActionUi.ts
Normal file
148
lib/vscode/src/vs/editor/contrib/codeAction/codeActionUi.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IAnchor } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Lazy } from 'vs/base/common/lazy';
|
||||
import { Disposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { CodeActionTriggerType } from 'vs/editor/common/modes';
|
||||
import { CodeActionItem, CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction';
|
||||
import { MessageController } from 'vs/editor/contrib/message/messageController';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { CodeActionMenu, CodeActionShowOptions } from './codeActionMenu';
|
||||
import { CodeActionsState } from './codeActionModel';
|
||||
import { LightBulbWidget } from './lightBulbWidget';
|
||||
import { CodeActionAutoApply, CodeActionTrigger } from './types';
|
||||
|
||||
export class CodeActionUi extends Disposable {
|
||||
|
||||
private readonly _codeActionWidget: Lazy<CodeActionMenu>;
|
||||
private readonly _lightBulbWidget: Lazy<LightBulbWidget>;
|
||||
private readonly _activeCodeActions = this._register(new MutableDisposable<CodeActionSet>());
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
quickFixActionId: string,
|
||||
preferredFixActionId: string,
|
||||
private readonly delegate: {
|
||||
applyCodeAction: (action: CodeActionItem, regtriggerAfterApply: boolean) => Promise<void>
|
||||
},
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._codeActionWidget = new Lazy(() => {
|
||||
return this._register(instantiationService.createInstance(CodeActionMenu, this._editor, {
|
||||
onSelectCodeAction: async (action) => {
|
||||
this.delegate.applyCodeAction(action, /* retrigger */ true);
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
this._lightBulbWidget = new Lazy(() => {
|
||||
const widget = this._register(instantiationService.createInstance(LightBulbWidget, this._editor, quickFixActionId, preferredFixActionId));
|
||||
this._register(widget.onClick(e => this.showCodeActionList(e.trigger, e.actions, e, { includeDisabledActions: false })));
|
||||
return widget;
|
||||
});
|
||||
}
|
||||
|
||||
public async update(newState: CodeActionsState.State): Promise<void> {
|
||||
if (newState.type !== CodeActionsState.Type.Triggered) {
|
||||
this._lightBulbWidget.rawValue?.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
let actions: CodeActionSet;
|
||||
try {
|
||||
actions = await newState.actions;
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
this._lightBulbWidget.getValue().update(actions, newState.trigger, newState.position);
|
||||
|
||||
if (newState.trigger.type === CodeActionTriggerType.Manual) {
|
||||
if (newState.trigger.filter?.include) { // Triggered for specific scope
|
||||
// Check to see if we want to auto apply.
|
||||
|
||||
const validActionToApply = this.tryGetValidActionToApply(newState.trigger, actions);
|
||||
if (validActionToApply) {
|
||||
try {
|
||||
await this.delegate.applyCodeAction(validActionToApply, false);
|
||||
} finally {
|
||||
actions.dispose();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if there is an action that we would have applied were it not invalid
|
||||
if (newState.trigger.context) {
|
||||
const invalidAction = this.getInvalidActionThatWouldHaveBeenApplied(newState.trigger, actions);
|
||||
if (invalidAction && invalidAction.action.disabled) {
|
||||
MessageController.get(this._editor).showMessage(invalidAction.action.disabled, newState.trigger.context.position);
|
||||
actions.dispose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const includeDisabledActions = !!newState.trigger.filter?.include;
|
||||
if (newState.trigger.context) {
|
||||
if (!actions.allActions.length || !includeDisabledActions && !actions.validActions.length) {
|
||||
MessageController.get(this._editor).showMessage(newState.trigger.context.notAvailableMessage, newState.trigger.context.position);
|
||||
this._activeCodeActions.value = actions;
|
||||
actions.dispose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this._activeCodeActions.value = actions;
|
||||
this._codeActionWidget.getValue().show(newState.trigger, actions, newState.position, { includeDisabledActions });
|
||||
} else {
|
||||
// auto magically triggered
|
||||
if (this._codeActionWidget.getValue().isVisible) {
|
||||
// TODO: Figure out if we should update the showing menu?
|
||||
actions.dispose();
|
||||
} else {
|
||||
this._activeCodeActions.value = actions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getInvalidActionThatWouldHaveBeenApplied(trigger: CodeActionTrigger, actions: CodeActionSet): CodeActionItem | undefined {
|
||||
if (!actions.allActions.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ((trigger.autoApply === CodeActionAutoApply.First && actions.validActions.length === 0)
|
||||
|| (trigger.autoApply === CodeActionAutoApply.IfSingle && actions.allActions.length === 1)
|
||||
) {
|
||||
return actions.allActions.find(({ action }) => action.disabled);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private tryGetValidActionToApply(trigger: CodeActionTrigger, actions: CodeActionSet): CodeActionItem | undefined {
|
||||
if (!actions.validActions.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ((trigger.autoApply === CodeActionAutoApply.First && actions.validActions.length > 0)
|
||||
|| (trigger.autoApply === CodeActionAutoApply.IfSingle && actions.validActions.length === 1)
|
||||
) {
|
||||
return actions.validActions[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public async showCodeActionList(trigger: CodeActionTrigger, actions: CodeActionSet, at: IAnchor | IPosition, options: CodeActionShowOptions): Promise<void> {
|
||||
this._codeActionWidget.getValue().show(trigger, actions, at, options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .contentWidgets .codicon-light-bulb,
|
||||
.monaco-editor .contentWidgets .codicon-lightbulb-autofix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.monaco-editor .contentWidgets .codicon-light-bulb:hover,
|
||||
.monaco-editor .contentWidgets .codicon-lightbulb-autofix:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
258
lib/vscode/src/vs/editor/contrib/codeAction/lightBulbWidget.ts
Normal file
258
lib/vscode/src/vs/editor/contrib/codeAction/lightBulbWidget.ts
Normal file
@@ -0,0 +1,258 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import 'vs/css!./lightBulbWidget';
|
||||
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from 'vs/editor/browser/editorBrowser';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { CodeActionSet } from 'vs/editor/contrib/codeAction/codeAction';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
|
||||
import { editorLightBulbForeground, editorLightBulbAutoFixForeground, editorBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { Gesture } from 'vs/base/browser/touch';
|
||||
import type { CodeActionTrigger } from 'vs/editor/contrib/codeAction/types';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
namespace LightBulbState {
|
||||
|
||||
export const enum Type {
|
||||
Hidden,
|
||||
Showing,
|
||||
}
|
||||
|
||||
export const Hidden = { type: Type.Hidden } as const;
|
||||
|
||||
export class Showing {
|
||||
readonly type = Type.Showing;
|
||||
|
||||
constructor(
|
||||
public readonly actions: CodeActionSet,
|
||||
public readonly trigger: CodeActionTrigger,
|
||||
public readonly editorPosition: IPosition,
|
||||
public readonly widgetPosition: IContentWidgetPosition,
|
||||
) { }
|
||||
}
|
||||
|
||||
export type State = typeof Hidden | Showing;
|
||||
}
|
||||
|
||||
|
||||
export class LightBulbWidget extends Disposable implements IContentWidget {
|
||||
|
||||
private static readonly _posPref = [ContentWidgetPositionPreference.EXACT];
|
||||
|
||||
private readonly _domNode: HTMLDivElement;
|
||||
|
||||
private readonly _onClick = this._register(new Emitter<{ x: number; y: number; actions: CodeActionSet; trigger: CodeActionTrigger }>());
|
||||
public readonly onClick = this._onClick.event;
|
||||
|
||||
private _state: LightBulbState.State = LightBulbState.Hidden;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
private readonly _quickFixActionId: string,
|
||||
private readonly _preferredFixActionId: string,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService
|
||||
) {
|
||||
super();
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = Codicon.lightBulb.classNames;
|
||||
|
||||
this._editor.addContentWidget(this);
|
||||
|
||||
this._register(this._editor.onDidChangeModelContent(_ => {
|
||||
// cancel when the line in question has been removed
|
||||
const editorModel = this._editor.getModel();
|
||||
if (this.state.type !== LightBulbState.Type.Showing || !editorModel || this.state.editorPosition.lineNumber >= editorModel.getLineCount()) {
|
||||
this.hide();
|
||||
}
|
||||
}));
|
||||
|
||||
Gesture.ignoreTarget(this._domNode);
|
||||
this._register(dom.addStandardDisposableGenericMouseDownListner(this._domNode, e => {
|
||||
if (this.state.type !== LightBulbState.Type.Showing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that focus / cursor location is not lost when clicking widget icon
|
||||
this._editor.focus();
|
||||
e.preventDefault();
|
||||
// a bit of extra work to make sure the menu
|
||||
// doesn't cover the line-text
|
||||
const { top, height } = dom.getDomNodePagePosition(this._domNode);
|
||||
const lineHeight = this._editor.getOption(EditorOption.lineHeight);
|
||||
|
||||
let pad = Math.floor(lineHeight / 3);
|
||||
if (this.state.widgetPosition.position !== null && this.state.widgetPosition.position.lineNumber < this.state.editorPosition.lineNumber) {
|
||||
pad += lineHeight;
|
||||
}
|
||||
|
||||
this._onClick.fire({
|
||||
x: e.posx,
|
||||
y: top + height + pad,
|
||||
actions: this.state.actions,
|
||||
trigger: this.state.trigger,
|
||||
});
|
||||
}));
|
||||
this._register(dom.addDisposableListener(this._domNode, 'mouseenter', (e: MouseEvent) => {
|
||||
if ((e.buttons & 1) !== 1) {
|
||||
return;
|
||||
}
|
||||
// mouse enters lightbulb while the primary/left button
|
||||
// is being pressed -> hide the lightbulb and block future
|
||||
// showings until mouse is released
|
||||
this.hide();
|
||||
const monitor = new GlobalMouseMoveMonitor<IStandardMouseMoveEventData>();
|
||||
monitor.startMonitoring(<HTMLElement>e.target, e.buttons, standardMouseMoveMerger, () => { }, () => {
|
||||
monitor.dispose();
|
||||
});
|
||||
}));
|
||||
this._register(this._editor.onDidChangeConfiguration(e => {
|
||||
// hide when told to do so
|
||||
if (e.hasChanged(EditorOption.lightbulb) && !this._editor.getOption(EditorOption.lightbulb).enabled) {
|
||||
this.hide();
|
||||
}
|
||||
}));
|
||||
|
||||
this._updateLightBulbTitleAndIcon();
|
||||
this._register(this._keybindingService.onDidUpdateKeybindings(this._updateLightBulbTitleAndIcon, this));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
super.dispose();
|
||||
this._editor.removeContentWidget(this);
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return 'LightBulbWidget';
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
getPosition(): IContentWidgetPosition | null {
|
||||
return this._state.type === LightBulbState.Type.Showing ? this._state.widgetPosition : null;
|
||||
}
|
||||
|
||||
public update(actions: CodeActionSet, trigger: CodeActionTrigger, atPosition: IPosition) {
|
||||
if (actions.validActions.length <= 0) {
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
const options = this._editor.getOptions();
|
||||
if (!options.get(EditorOption.lightbulb).enabled) {
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return this.hide();
|
||||
}
|
||||
|
||||
const { lineNumber, column } = model.validatePosition(atPosition);
|
||||
|
||||
const tabSize = model.getOptions().tabSize;
|
||||
const fontInfo = options.get(EditorOption.fontInfo);
|
||||
const lineContent = model.getLineContent(lineNumber);
|
||||
const indent = TextModel.computeIndentLevel(lineContent, tabSize);
|
||||
const lineHasSpace = fontInfo.spaceWidth * indent > 22;
|
||||
const isFolded = (lineNumber: number) => {
|
||||
return lineNumber > 2 && this._editor.getTopForLineNumber(lineNumber) === this._editor.getTopForLineNumber(lineNumber - 1);
|
||||
};
|
||||
|
||||
let effectiveLineNumber = lineNumber;
|
||||
if (!lineHasSpace) {
|
||||
if (lineNumber > 1 && !isFolded(lineNumber - 1)) {
|
||||
effectiveLineNumber -= 1;
|
||||
} else if (!isFolded(lineNumber + 1)) {
|
||||
effectiveLineNumber += 1;
|
||||
} else if (column * fontInfo.spaceWidth < 22) {
|
||||
// cannot show lightbulb above/below and showing
|
||||
// it inline would overlay the cursor...
|
||||
return this.hide();
|
||||
}
|
||||
}
|
||||
|
||||
this.state = new LightBulbState.Showing(actions, trigger, atPosition, {
|
||||
position: { lineNumber: effectiveLineNumber, column: 1 },
|
||||
preference: LightBulbWidget._posPref
|
||||
});
|
||||
this._editor.layoutContentWidget(this);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
this.state = LightBulbState.Hidden;
|
||||
this._editor.layoutContentWidget(this);
|
||||
}
|
||||
|
||||
private get state(): LightBulbState.State { return this._state; }
|
||||
|
||||
private set state(value) {
|
||||
this._state = value;
|
||||
this._updateLightBulbTitleAndIcon();
|
||||
}
|
||||
|
||||
private _updateLightBulbTitleAndIcon(): void {
|
||||
if (this.state.type === LightBulbState.Type.Showing && this.state.actions.hasAutoFix) {
|
||||
// update icon
|
||||
this._domNode.classList.remove(...Codicon.lightBulb.classNamesArray);
|
||||
this._domNode.classList.add(...Codicon.lightbulbAutofix.classNamesArray);
|
||||
|
||||
const preferredKb = this._keybindingService.lookupKeybinding(this._preferredFixActionId);
|
||||
if (preferredKb) {
|
||||
this.title = nls.localize('prefferedQuickFixWithKb', "Show Fixes. Preferred Fix Available ({0})", preferredKb.getLabel());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// update icon
|
||||
this._domNode.classList.remove(...Codicon.lightbulbAutofix.classNamesArray);
|
||||
this._domNode.classList.add(...Codicon.lightBulb.classNamesArray);
|
||||
|
||||
const kb = this._keybindingService.lookupKeybinding(this._quickFixActionId);
|
||||
if (kb) {
|
||||
this.title = nls.localize('quickFixWithKb', "Show Fixes ({0})", kb.getLabel());
|
||||
} else {
|
||||
this.title = nls.localize('quickFix', "Show Fixes");
|
||||
}
|
||||
}
|
||||
|
||||
private set title(value: string) {
|
||||
this._domNode.title = value;
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
|
||||
|
||||
const editorBackgroundColor = theme.getColor(editorBackground)?.transparent(0.7);
|
||||
|
||||
// Lightbulb Icon
|
||||
const editorLightBulbForegroundColor = theme.getColor(editorLightBulbForeground);
|
||||
if (editorLightBulbForegroundColor) {
|
||||
collector.addRule(`
|
||||
.monaco-editor .contentWidgets ${Codicon.lightBulb.cssSelector} {
|
||||
color: ${editorLightBulbForegroundColor};
|
||||
background-color: ${editorBackgroundColor};
|
||||
}`);
|
||||
}
|
||||
|
||||
// Lightbulb Auto Fix Icon
|
||||
const editorLightBulbAutoFixForegroundColor = theme.getColor(editorLightBulbAutoFixForeground);
|
||||
if (editorLightBulbAutoFixForegroundColor) {
|
||||
collector.addRule(`
|
||||
.monaco-editor .contentWidgets ${Codicon.lightbulbAutofix.cssSelector} {
|
||||
color: ${editorLightBulbAutoFixForegroundColor};
|
||||
background-color: ${editorBackgroundColor};
|
||||
}`);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,284 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { CodeActionItem, getCodeActions } from 'vs/editor/contrib/codeAction/codeAction';
|
||||
import { CodeActionKind } from 'vs/editor/contrib/codeAction/types';
|
||||
import { IMarkerData, MarkerSeverity } from 'vs/platform/markers/common/markers';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { Progress } from 'vs/platform/progress/common/progress';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
|
||||
function staticCodeActionProvider(...actions: modes.CodeAction[]): modes.CodeActionProvider {
|
||||
return new class implements modes.CodeActionProvider {
|
||||
provideCodeActions(): modes.CodeActionList {
|
||||
return {
|
||||
actions: actions,
|
||||
dispose: () => { }
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
suite('CodeAction', () => {
|
||||
|
||||
let langId = new modes.LanguageIdentifier('fooLang', 17);
|
||||
let uri = URI.parse('untitled:path');
|
||||
let model: TextModel;
|
||||
const disposables = new DisposableStore();
|
||||
let testData = {
|
||||
diagnostics: {
|
||||
abc: {
|
||||
title: 'bTitle',
|
||||
diagnostics: [{
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 2,
|
||||
endColumn: 1,
|
||||
severity: MarkerSeverity.Error,
|
||||
message: 'abc'
|
||||
}]
|
||||
},
|
||||
bcd: {
|
||||
title: 'aTitle',
|
||||
diagnostics: [{
|
||||
startLineNumber: 1,
|
||||
startColumn: 1,
|
||||
endLineNumber: 2,
|
||||
endColumn: 1,
|
||||
severity: MarkerSeverity.Error,
|
||||
message: 'bcd'
|
||||
}]
|
||||
}
|
||||
},
|
||||
command: {
|
||||
abc: {
|
||||
command: new class implements modes.Command {
|
||||
id!: '1';
|
||||
title!: 'abc';
|
||||
},
|
||||
title: 'Extract to inner function in function "test"'
|
||||
}
|
||||
},
|
||||
spelling: {
|
||||
bcd: {
|
||||
diagnostics: <IMarkerData[]>[],
|
||||
edit: new class implements modes.WorkspaceEdit {
|
||||
edits!: modes.WorkspaceTextEdit[];
|
||||
},
|
||||
title: 'abc'
|
||||
}
|
||||
},
|
||||
tsLint: {
|
||||
abc: {
|
||||
$ident: 57,
|
||||
arguments: <IMarkerData[]>[],
|
||||
id: '_internal_command_delegation',
|
||||
title: 'abc'
|
||||
},
|
||||
bcd: {
|
||||
$ident: 47,
|
||||
arguments: <IMarkerData[]>[],
|
||||
id: '_internal_command_delegation',
|
||||
title: 'bcd'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setup(function () {
|
||||
disposables.clear();
|
||||
model = createTextModel('test1\ntest2\ntest3', undefined, langId, uri);
|
||||
disposables.add(model);
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
disposables.clear();
|
||||
});
|
||||
|
||||
test('CodeActions are sorted by type, #38623', async function () {
|
||||
|
||||
const provider = staticCodeActionProvider(
|
||||
testData.command.abc,
|
||||
testData.diagnostics.bcd,
|
||||
testData.spelling.bcd,
|
||||
testData.tsLint.bcd,
|
||||
testData.tsLint.abc,
|
||||
testData.diagnostics.abc
|
||||
);
|
||||
|
||||
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
const expected = [
|
||||
// CodeActions with a diagnostics array are shown first ordered by diagnostics.message
|
||||
new CodeActionItem(testData.diagnostics.abc, provider),
|
||||
new CodeActionItem(testData.diagnostics.bcd, provider),
|
||||
|
||||
// CodeActions without diagnostics are shown in the given order without any further sorting
|
||||
new CodeActionItem(testData.command.abc, provider),
|
||||
new CodeActionItem(testData.spelling.bcd, provider), // empty diagnostics array
|
||||
new CodeActionItem(testData.tsLint.bcd, provider),
|
||||
new CodeActionItem(testData.tsLint.abc, provider)
|
||||
];
|
||||
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Manual }, Progress.None, CancellationToken.None);
|
||||
assert.equal(actions.length, 6);
|
||||
assert.deepEqual(actions, expected);
|
||||
});
|
||||
|
||||
test('getCodeActions should filter by scope', async function () {
|
||||
const provider = staticCodeActionProvider(
|
||||
{ title: 'a', kind: 'a' },
|
||||
{ title: 'b', kind: 'b' },
|
||||
{ title: 'a.b', kind: 'a.b' }
|
||||
);
|
||||
|
||||
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
{
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: new CodeActionKind('a') } }, Progress.None, CancellationToken.None);
|
||||
assert.equal(actions.length, 2);
|
||||
assert.strictEqual(actions[0].action.title, 'a');
|
||||
assert.strictEqual(actions[1].action.title, 'a.b');
|
||||
}
|
||||
|
||||
{
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: new CodeActionKind('a.b') } }, Progress.None, CancellationToken.None);
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].action.title, 'a.b');
|
||||
}
|
||||
|
||||
{
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: new CodeActionKind('a.b.c') } }, Progress.None, CancellationToken.None);
|
||||
assert.equal(actions.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
test('getCodeActions should forward requested scope to providers', async function () {
|
||||
const provider = new class implements modes.CodeActionProvider {
|
||||
provideCodeActions(_model: any, _range: Range, context: modes.CodeActionContext, _token: any): modes.CodeActionList {
|
||||
return {
|
||||
actions: [
|
||||
{ title: context.only || '', kind: context.only }
|
||||
],
|
||||
dispose: () => { }
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: new CodeActionKind('a') } }, Progress.None, CancellationToken.None);
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].action.title, 'a');
|
||||
});
|
||||
|
||||
test('getCodeActions should not return source code action by default', async function () {
|
||||
const provider = staticCodeActionProvider(
|
||||
{ title: 'a', kind: CodeActionKind.Source.value },
|
||||
{ title: 'b', kind: 'b' }
|
||||
);
|
||||
|
||||
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
{
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto }, Progress.None, CancellationToken.None);
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].action.title, 'b');
|
||||
}
|
||||
|
||||
{
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: modes.CodeActionTriggerType.Auto, filter: { include: CodeActionKind.Source, includeSourceActions: true } }, Progress.None, CancellationToken.None);
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].action.title, 'a');
|
||||
}
|
||||
});
|
||||
|
||||
test('getCodeActions should support filtering out some requested source code actions #84602', async function () {
|
||||
const provider = staticCodeActionProvider(
|
||||
{ title: 'a', kind: CodeActionKind.Source.value },
|
||||
{ title: 'b', kind: CodeActionKind.Source.append('test').value },
|
||||
{ title: 'c', kind: 'c' }
|
||||
);
|
||||
|
||||
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
{
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), {
|
||||
type: modes.CodeActionTriggerType.Auto, filter: {
|
||||
include: CodeActionKind.Source.append('test'),
|
||||
excludes: [CodeActionKind.Source],
|
||||
includeSourceActions: true,
|
||||
}
|
||||
}, Progress.None, CancellationToken.None);
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].action.title, 'b');
|
||||
}
|
||||
});
|
||||
|
||||
test('getCodeActions no invoke a provider that has been excluded #84602', async function () {
|
||||
const baseType = CodeActionKind.Refactor;
|
||||
const subType = CodeActionKind.Refactor.append('sub');
|
||||
|
||||
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', staticCodeActionProvider(
|
||||
{ title: 'a', kind: baseType.value }
|
||||
)));
|
||||
|
||||
let didInvoke = false;
|
||||
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', new class implements modes.CodeActionProvider {
|
||||
|
||||
providedCodeActionKinds = [subType.value];
|
||||
|
||||
provideCodeActions(): modes.ProviderResult<modes.CodeActionList> {
|
||||
didInvoke = true;
|
||||
return {
|
||||
actions: [
|
||||
{ title: 'x', kind: subType.value }
|
||||
],
|
||||
dispose: () => { }
|
||||
};
|
||||
}
|
||||
}));
|
||||
|
||||
{
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), {
|
||||
type: modes.CodeActionTriggerType.Auto, filter: {
|
||||
include: baseType,
|
||||
excludes: [subType],
|
||||
}
|
||||
}, Progress.None, CancellationToken.None);
|
||||
assert.strictEqual(didInvoke, false);
|
||||
assert.equal(actions.length, 1);
|
||||
assert.strictEqual(actions[0].action.title, 'a');
|
||||
}
|
||||
});
|
||||
|
||||
test('getCodeActions should not invoke code action providers filtered out by providedCodeActionKinds', async function () {
|
||||
let wasInvoked = false;
|
||||
const provider = new class implements modes.CodeActionProvider {
|
||||
provideCodeActions(): modes.CodeActionList {
|
||||
wasInvoked = true;
|
||||
return { actions: [], dispose: () => { } };
|
||||
}
|
||||
|
||||
providedCodeActionKinds = [CodeActionKind.Refactor.value];
|
||||
};
|
||||
|
||||
disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider));
|
||||
|
||||
const { validActions: actions } = await getCodeActions(model, new Range(1, 1, 2, 1), {
|
||||
type: modes.CodeActionTriggerType.Auto,
|
||||
filter: {
|
||||
include: CodeActionKind.QuickFix
|
||||
}
|
||||
}, Progress.None, CancellationToken.None);
|
||||
assert.strictEqual(actions.length, 0);
|
||||
assert.strictEqual(wasInvoked, false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ChordKeybinding, KeyCode, SimpleKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { refactorCommandId, organizeImportsCommandId } from 'vs/editor/contrib/codeAction/codeAction';
|
||||
import { CodeActionKind } from 'vs/editor/contrib/codeAction/types';
|
||||
import { CodeActionKeybindingResolver } from 'vs/editor/contrib/codeAction/codeActionMenu';
|
||||
import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem';
|
||||
import { USLayoutResolvedKeybinding } from 'vs/platform/keybinding/common/usLayoutResolvedKeybinding';
|
||||
|
||||
suite('CodeActionKeybindingResolver', () => {
|
||||
const refactorKeybinding = createCodeActionKeybinding(
|
||||
KeyCode.KEY_A,
|
||||
refactorCommandId,
|
||||
{ kind: CodeActionKind.Refactor.value });
|
||||
|
||||
const refactorExtractKeybinding = createCodeActionKeybinding(
|
||||
KeyCode.KEY_B,
|
||||
refactorCommandId,
|
||||
{ kind: CodeActionKind.Refactor.append('extract').value });
|
||||
|
||||
const organizeImportsKeybinding = createCodeActionKeybinding(
|
||||
KeyCode.KEY_C,
|
||||
organizeImportsCommandId,
|
||||
undefined);
|
||||
|
||||
test('Should match refactor keybindings', async function () {
|
||||
const resolver = new CodeActionKeybindingResolver({
|
||||
getKeybindings: (): readonly ResolvedKeybindingItem[] => {
|
||||
return [refactorKeybinding];
|
||||
},
|
||||
}).getResolver();
|
||||
|
||||
assert.equal(
|
||||
resolver({ title: '' }),
|
||||
undefined);
|
||||
|
||||
assert.equal(
|
||||
resolver({ title: '', kind: CodeActionKind.Refactor.value }),
|
||||
refactorKeybinding.resolvedKeybinding);
|
||||
|
||||
assert.equal(
|
||||
resolver({ title: '', kind: CodeActionKind.Refactor.append('extract').value }),
|
||||
refactorKeybinding.resolvedKeybinding);
|
||||
|
||||
assert.equal(
|
||||
resolver({ title: '', kind: CodeActionKind.QuickFix.value }),
|
||||
undefined);
|
||||
});
|
||||
|
||||
test('Should prefer most specific keybinding', async function () {
|
||||
const resolver = new CodeActionKeybindingResolver({
|
||||
getKeybindings: (): readonly ResolvedKeybindingItem[] => {
|
||||
return [refactorKeybinding, refactorExtractKeybinding, organizeImportsKeybinding];
|
||||
},
|
||||
}).getResolver();
|
||||
|
||||
assert.equal(
|
||||
resolver({ title: '', kind: CodeActionKind.Refactor.value }),
|
||||
refactorKeybinding.resolvedKeybinding);
|
||||
|
||||
assert.equal(
|
||||
resolver({ title: '', kind: CodeActionKind.Refactor.append('extract').value }),
|
||||
refactorExtractKeybinding.resolvedKeybinding);
|
||||
});
|
||||
|
||||
test('Organize imports should still return a keybinding even though it does not have args', async function () {
|
||||
const resolver = new CodeActionKeybindingResolver({
|
||||
getKeybindings: (): readonly ResolvedKeybindingItem[] => {
|
||||
return [refactorKeybinding, refactorExtractKeybinding, organizeImportsKeybinding];
|
||||
},
|
||||
}).getResolver();
|
||||
|
||||
assert.equal(
|
||||
resolver({ title: '', kind: CodeActionKind.SourceOrganizeImports.value }),
|
||||
organizeImportsKeybinding.resolvedKeybinding);
|
||||
});
|
||||
});
|
||||
|
||||
function createCodeActionKeybinding(keycode: KeyCode, command: string, commandArgs: any) {
|
||||
return new ResolvedKeybindingItem(
|
||||
new USLayoutResolvedKeybinding(
|
||||
new ChordKeybinding([new SimpleKeybinding(false, true, false, false, keycode)]),
|
||||
OperatingSystem.Linux),
|
||||
command,
|
||||
commandArgs,
|
||||
undefined,
|
||||
false,
|
||||
null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { assertType } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { CodeActionModel, CodeActionsState } from 'vs/editor/contrib/codeAction/codeActionModel';
|
||||
import { createTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
|
||||
import { MarkerService } from 'vs/platform/markers/common/markerService';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
|
||||
const testProvider = {
|
||||
provideCodeActions(): modes.CodeActionList {
|
||||
return {
|
||||
actions: [
|
||||
{ title: 'test', command: { id: 'test-command', title: 'test', arguments: [] } }
|
||||
],
|
||||
dispose() { /* noop*/ }
|
||||
};
|
||||
}
|
||||
};
|
||||
suite('CodeActionModel', () => {
|
||||
|
||||
const languageIdentifier = new modes.LanguageIdentifier('foo-lang', 3);
|
||||
let uri = URI.parse('untitled:path');
|
||||
let model: TextModel;
|
||||
let markerService: MarkerService;
|
||||
let editor: ICodeEditor;
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
setup(() => {
|
||||
disposables.clear();
|
||||
markerService = new MarkerService();
|
||||
model = createTextModel('foobar foo bar\nfarboo far boo', undefined, languageIdentifier, uri);
|
||||
editor = createTestCodeEditor({ model: model });
|
||||
editor.setPosition({ lineNumber: 1, column: 1 });
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
disposables.clear();
|
||||
editor.dispose();
|
||||
model.dispose();
|
||||
markerService.dispose();
|
||||
});
|
||||
|
||||
test('Orcale -> marker added', done => {
|
||||
const reg = modes.CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
|
||||
disposables.add(reg);
|
||||
|
||||
const contextKeys = new MockContextKeyService();
|
||||
const model = disposables.add(new CodeActionModel(editor, markerService, contextKeys, undefined));
|
||||
disposables.add(model.onDidChangeState((e: CodeActionsState.State) => {
|
||||
assertType(e.type === CodeActionsState.Type.Triggered);
|
||||
|
||||
assert.strictEqual(e.trigger.type, modes.CodeActionTriggerType.Auto);
|
||||
assert.ok(e.actions);
|
||||
|
||||
e.actions.then(fixes => {
|
||||
model.dispose();
|
||||
assert.equal(fixes.validActions.length, 1);
|
||||
done();
|
||||
}, done);
|
||||
}));
|
||||
|
||||
// start here
|
||||
markerService.changeOne('fake', uri, [{
|
||||
startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6,
|
||||
message: 'error',
|
||||
severity: 1,
|
||||
code: '',
|
||||
source: ''
|
||||
}]);
|
||||
|
||||
});
|
||||
|
||||
test('Orcale -> position changed', () => {
|
||||
const reg = modes.CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
|
||||
disposables.add(reg);
|
||||
|
||||
markerService.changeOne('fake', uri, [{
|
||||
startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6,
|
||||
message: 'error',
|
||||
severity: 1,
|
||||
code: '',
|
||||
source: ''
|
||||
}]);
|
||||
|
||||
editor.setPosition({ lineNumber: 2, column: 1 });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const contextKeys = new MockContextKeyService();
|
||||
const model = disposables.add(new CodeActionModel(editor, markerService, contextKeys, undefined));
|
||||
disposables.add(model.onDidChangeState((e: CodeActionsState.State) => {
|
||||
assertType(e.type === CodeActionsState.Type.Triggered);
|
||||
|
||||
assert.equal(e.trigger.type, modes.CodeActionTriggerType.Auto);
|
||||
assert.ok(e.actions);
|
||||
e.actions.then(fixes => {
|
||||
model.dispose();
|
||||
assert.equal(fixes.validActions.length, 1);
|
||||
resolve(undefined);
|
||||
}, reject);
|
||||
}));
|
||||
// start here
|
||||
editor.setPosition({ lineNumber: 1, column: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
test('Lightbulb is in the wrong place, #29933', async function () {
|
||||
const reg = modes.CodeActionProviderRegistry.register(languageIdentifier.language, {
|
||||
provideCodeActions(_doc, _range): modes.CodeActionList {
|
||||
return { actions: [], dispose() { /* noop*/ } };
|
||||
}
|
||||
});
|
||||
disposables.add(reg);
|
||||
|
||||
editor.getModel()!.setValue('// @ts-check\n2\ncon\n');
|
||||
|
||||
markerService.changeOne('fake', uri, [{
|
||||
startLineNumber: 3, startColumn: 1, endLineNumber: 3, endColumn: 4,
|
||||
message: 'error',
|
||||
severity: 1,
|
||||
code: '',
|
||||
source: ''
|
||||
}]);
|
||||
|
||||
// case 1 - drag selection over multiple lines -> range of enclosed marker, position or marker
|
||||
await new Promise(resolve => {
|
||||
const contextKeys = new MockContextKeyService();
|
||||
const model = disposables.add(new CodeActionModel(editor, markerService, contextKeys, undefined));
|
||||
disposables.add(model.onDidChangeState((e: CodeActionsState.State) => {
|
||||
assertType(e.type === CodeActionsState.Type.Triggered);
|
||||
|
||||
assert.equal(e.trigger.type, modes.CodeActionTriggerType.Auto);
|
||||
const selection = <Selection>e.rangeOrSelection;
|
||||
assert.deepEqual(selection.selectionStartLineNumber, 1);
|
||||
assert.deepEqual(selection.selectionStartColumn, 1);
|
||||
assert.deepEqual(selection.endLineNumber, 4);
|
||||
assert.deepEqual(selection.endColumn, 1);
|
||||
assert.deepEqual(e.position, { lineNumber: 3, column: 1 });
|
||||
model.dispose();
|
||||
resolve(undefined);
|
||||
}, 5));
|
||||
|
||||
editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
test('Orcale -> should only auto trigger once for cursor and marker update right after each other', done => {
|
||||
const reg = modes.CodeActionProviderRegistry.register(languageIdentifier.language, testProvider);
|
||||
disposables.add(reg);
|
||||
|
||||
let triggerCount = 0;
|
||||
const contextKeys = new MockContextKeyService();
|
||||
const model = disposables.add(new CodeActionModel(editor, markerService, contextKeys, undefined));
|
||||
disposables.add(model.onDidChangeState((e: CodeActionsState.State) => {
|
||||
assertType(e.type === CodeActionsState.Type.Triggered);
|
||||
|
||||
assert.equal(e.trigger.type, modes.CodeActionTriggerType.Auto);
|
||||
++triggerCount;
|
||||
|
||||
// give time for second trigger before completing test
|
||||
setTimeout(() => {
|
||||
model.dispose();
|
||||
assert.strictEqual(triggerCount, 1);
|
||||
done();
|
||||
}, 50);
|
||||
}, 5 /*delay*/));
|
||||
|
||||
markerService.changeOne('fake', uri, [{
|
||||
startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 6,
|
||||
message: 'error',
|
||||
severity: 1,
|
||||
code: '',
|
||||
source: ''
|
||||
}]);
|
||||
|
||||
editor.setSelection({ startLineNumber: 1, startColumn: 1, endLineNumber: 4, endColumn: 1 });
|
||||
});
|
||||
});
|
||||
164
lib/vscode/src/vs/editor/contrib/codeAction/types.ts
Normal file
164
lib/vscode/src/vs/editor/contrib/codeAction/types.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CodeAction, CodeActionTriggerType } from 'vs/editor/common/modes';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
|
||||
export class CodeActionKind {
|
||||
private static readonly sep = '.';
|
||||
|
||||
public static readonly None = new CodeActionKind('@@none@@'); // Special code action that contains nothing
|
||||
public static readonly Empty = new CodeActionKind('');
|
||||
public static readonly QuickFix = new CodeActionKind('quickfix');
|
||||
public static readonly Refactor = new CodeActionKind('refactor');
|
||||
public static readonly Source = new CodeActionKind('source');
|
||||
public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports');
|
||||
public static readonly SourceFixAll = CodeActionKind.Source.append('fixAll');
|
||||
|
||||
constructor(
|
||||
public readonly value: string
|
||||
) { }
|
||||
|
||||
public equals(other: CodeActionKind): boolean {
|
||||
return this.value === other.value;
|
||||
}
|
||||
|
||||
public contains(other: CodeActionKind): boolean {
|
||||
return this.equals(other) || this.value === '' || other.value.startsWith(this.value + CodeActionKind.sep);
|
||||
}
|
||||
|
||||
public intersects(other: CodeActionKind): boolean {
|
||||
return this.contains(other) || other.contains(this);
|
||||
}
|
||||
|
||||
public append(part: string): CodeActionKind {
|
||||
return new CodeActionKind(this.value + CodeActionKind.sep + part);
|
||||
}
|
||||
}
|
||||
|
||||
export const enum CodeActionAutoApply {
|
||||
IfSingle = 'ifSingle',
|
||||
First = 'first',
|
||||
Never = 'never',
|
||||
}
|
||||
|
||||
export interface CodeActionFilter {
|
||||
readonly include?: CodeActionKind;
|
||||
readonly excludes?: readonly CodeActionKind[];
|
||||
readonly includeSourceActions?: boolean;
|
||||
readonly onlyIncludePreferredActions?: boolean;
|
||||
}
|
||||
|
||||
export function mayIncludeActionsOfKind(filter: CodeActionFilter, providedKind: CodeActionKind): boolean {
|
||||
// A provided kind may be a subset or superset of our filtered kind.
|
||||
if (filter.include && !filter.include.intersects(providedKind)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filter.excludes) {
|
||||
if (filter.excludes.some(exclude => excludesAction(providedKind, exclude, filter.include))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't return source actions unless they are explicitly requested
|
||||
if (!filter.includeSourceActions && CodeActionKind.Source.contains(providedKind)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function filtersAction(filter: CodeActionFilter, action: CodeAction): boolean {
|
||||
const actionKind = action.kind ? new CodeActionKind(action.kind) : undefined;
|
||||
|
||||
// Filter out actions by kind
|
||||
if (filter.include) {
|
||||
if (!actionKind || !filter.include.contains(actionKind)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.excludes) {
|
||||
if (actionKind && filter.excludes.some(exclude => excludesAction(actionKind, exclude, filter.include))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't return source actions unless they are explicitly requested
|
||||
if (!filter.includeSourceActions) {
|
||||
if (actionKind && CodeActionKind.Source.contains(actionKind)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter.onlyIncludePreferredActions) {
|
||||
if (!action.isPreferred) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function excludesAction(providedKind: CodeActionKind, exclude: CodeActionKind, include: CodeActionKind | undefined): boolean {
|
||||
if (!exclude.contains(providedKind)) {
|
||||
return false;
|
||||
}
|
||||
if (include && exclude.contains(include)) {
|
||||
// The include is more specific, don't filter out
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export interface CodeActionTrigger {
|
||||
readonly type: CodeActionTriggerType;
|
||||
readonly filter?: CodeActionFilter;
|
||||
readonly autoApply?: CodeActionAutoApply;
|
||||
readonly context?: {
|
||||
readonly notAvailableMessage: string;
|
||||
readonly position: Position;
|
||||
};
|
||||
}
|
||||
|
||||
export class CodeActionCommandArgs {
|
||||
public static fromUser(arg: any, defaults: { kind: CodeActionKind, apply: CodeActionAutoApply }): CodeActionCommandArgs {
|
||||
if (!arg || typeof arg !== 'object') {
|
||||
return new CodeActionCommandArgs(defaults.kind, defaults.apply, false);
|
||||
}
|
||||
return new CodeActionCommandArgs(
|
||||
CodeActionCommandArgs.getKindFromUser(arg, defaults.kind),
|
||||
CodeActionCommandArgs.getApplyFromUser(arg, defaults.apply),
|
||||
CodeActionCommandArgs.getPreferredUser(arg));
|
||||
}
|
||||
|
||||
private static getApplyFromUser(arg: any, defaultAutoApply: CodeActionAutoApply) {
|
||||
switch (typeof arg.apply === 'string' ? arg.apply.toLowerCase() : '') {
|
||||
case 'first': return CodeActionAutoApply.First;
|
||||
case 'never': return CodeActionAutoApply.Never;
|
||||
case 'ifsingle': return CodeActionAutoApply.IfSingle;
|
||||
default: return defaultAutoApply;
|
||||
}
|
||||
}
|
||||
|
||||
private static getKindFromUser(arg: any, defaultKind: CodeActionKind) {
|
||||
return typeof arg.kind === 'string'
|
||||
? new CodeActionKind(arg.kind)
|
||||
: defaultKind;
|
||||
}
|
||||
|
||||
private static getPreferredUser(arg: any): boolean {
|
||||
return typeof arg.preferred === 'boolean'
|
||||
? arg.preferred
|
||||
: false;
|
||||
}
|
||||
|
||||
private constructor(
|
||||
public readonly kind: CodeActionKind,
|
||||
public readonly apply: CodeActionAutoApply,
|
||||
public readonly preferred: boolean,
|
||||
) { }
|
||||
}
|
||||
132
lib/vscode/src/vs/editor/contrib/codelens/codeLensCache.ts
Normal file
132
lib/vscode/src/vs/editor/contrib/codelens/codeLensCache.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { CodeLensModel } from 'vs/editor/contrib/codelens/codelens';
|
||||
import { LRUCache } from 'vs/base/common/map';
|
||||
import { CodeLensProvider, CodeLensList, CodeLens } from 'vs/editor/common/modes';
|
||||
import { IStorageService, StorageScope, WillSaveStateReason } from 'vs/platform/storage/common/storage';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { runWhenIdle } from 'vs/base/common/async';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
|
||||
export const ICodeLensCache = createDecorator<ICodeLensCache>('ICodeLensCache');
|
||||
|
||||
export interface ICodeLensCache {
|
||||
readonly _serviceBrand: undefined;
|
||||
put(model: ITextModel, data: CodeLensModel): void;
|
||||
get(model: ITextModel): CodeLensModel | undefined;
|
||||
delete(model: ITextModel): void;
|
||||
}
|
||||
|
||||
interface ISerializedCacheData {
|
||||
lineCount: number;
|
||||
lines: number[];
|
||||
}
|
||||
|
||||
class CacheItem {
|
||||
|
||||
constructor(
|
||||
readonly lineCount: number,
|
||||
readonly data: CodeLensModel
|
||||
) { }
|
||||
}
|
||||
|
||||
export class CodeLensCache implements ICodeLensCache {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _fakeProvider = new class implements CodeLensProvider {
|
||||
provideCodeLenses(): CodeLensList {
|
||||
throw new Error('not supported');
|
||||
}
|
||||
};
|
||||
|
||||
private readonly _cache = new LRUCache<string, CacheItem>(20, 0.75);
|
||||
|
||||
constructor(@IStorageService storageService: IStorageService) {
|
||||
|
||||
// remove old data
|
||||
const oldkey = 'codelens/cache';
|
||||
runWhenIdle(() => storageService.remove(oldkey, StorageScope.WORKSPACE));
|
||||
|
||||
// restore lens data on start
|
||||
const key = 'codelens/cache2';
|
||||
const raw = storageService.get(key, StorageScope.WORKSPACE, '{}');
|
||||
this._deserialize(raw);
|
||||
|
||||
// store lens data on shutdown
|
||||
once(storageService.onWillSaveState)(e => {
|
||||
if (e.reason === WillSaveStateReason.SHUTDOWN) {
|
||||
storageService.store(key, this._serialize(), StorageScope.WORKSPACE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
put(model: ITextModel, data: CodeLensModel): void {
|
||||
// create a copy of the model that is without command-ids
|
||||
// but with comand-labels
|
||||
const copyItems = data.lenses.map(item => {
|
||||
return <CodeLens>{
|
||||
range: item.symbol.range,
|
||||
command: item.symbol.command && { id: '', title: item.symbol.command?.title },
|
||||
};
|
||||
});
|
||||
const copyModel = new CodeLensModel();
|
||||
copyModel.add({ lenses: copyItems, dispose: () => { } }, this._fakeProvider);
|
||||
|
||||
const item = new CacheItem(model.getLineCount(), copyModel);
|
||||
this._cache.set(model.uri.toString(), item);
|
||||
}
|
||||
|
||||
get(model: ITextModel) {
|
||||
const item = this._cache.get(model.uri.toString());
|
||||
return item && item.lineCount === model.getLineCount() ? item.data : undefined;
|
||||
}
|
||||
|
||||
delete(model: ITextModel): void {
|
||||
this._cache.delete(model.uri.toString());
|
||||
}
|
||||
|
||||
// --- persistence
|
||||
|
||||
private _serialize(): string {
|
||||
const data: Record<string, ISerializedCacheData> = Object.create(null);
|
||||
for (const [key, value] of this._cache) {
|
||||
const lines = new Set<number>();
|
||||
for (const d of value.data.lenses) {
|
||||
lines.add(d.symbol.range.startLineNumber);
|
||||
}
|
||||
data[key] = {
|
||||
lineCount: value.lineCount,
|
||||
lines: [...lines.values()]
|
||||
};
|
||||
}
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
|
||||
private _deserialize(raw: string): void {
|
||||
try {
|
||||
const data: Record<string, ISerializedCacheData> = JSON.parse(raw);
|
||||
for (const key in data) {
|
||||
const element = data[key];
|
||||
const lenses: CodeLens[] = [];
|
||||
for (const line of element.lines) {
|
||||
lenses.push({ range: new Range(line, 1, line, 11) });
|
||||
}
|
||||
|
||||
const model = new CodeLensModel();
|
||||
model.add({ lenses, dispose() { } }, this._fakeProvider);
|
||||
this._cache.set(key, new CacheItem(element.lineCount, model));
|
||||
}
|
||||
} catch {
|
||||
// ignore...
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ICodeLensCache, CodeLensCache);
|
||||
118
lib/vscode/src/vs/editor/contrib/codelens/codelens.ts
Normal file
118
lib/vscode/src/vs/editor/contrib/codelens/codelens.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { mergeSort } from 'vs/base/common/arrays';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { CodeLensProvider, CodeLensProviderRegistry, CodeLens, CodeLensList } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface CodeLensItem {
|
||||
symbol: CodeLens;
|
||||
provider: CodeLensProvider;
|
||||
}
|
||||
|
||||
export class CodeLensModel {
|
||||
|
||||
lenses: CodeLensItem[] = [];
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
add(list: CodeLensList, provider: CodeLensProvider): void {
|
||||
this._disposables.add(list);
|
||||
for (const symbol of list.lenses) {
|
||||
this.lenses.push({ symbol, provider });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCodeLensModel(model: ITextModel, token: CancellationToken): Promise<CodeLensModel> {
|
||||
|
||||
const provider = CodeLensProviderRegistry.ordered(model);
|
||||
const providerRanks = new Map<CodeLensProvider, number>();
|
||||
const result = new CodeLensModel();
|
||||
|
||||
const promises = provider.map(async (provider, i) => {
|
||||
|
||||
providerRanks.set(provider, i);
|
||||
|
||||
try {
|
||||
const list = await Promise.resolve(provider.provideCodeLenses(model, token));
|
||||
if (list) {
|
||||
result.add(list, provider);
|
||||
}
|
||||
} catch (err) {
|
||||
onUnexpectedExternalError(err);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
result.lenses = mergeSort(result.lenses, (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 ((providerRanks.get(a.provider)!) < (providerRanks.get(b.provider)!)) {
|
||||
return -1;
|
||||
} else if ((providerRanks.get(a.provider)!) > (providerRanks.get(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;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
registerLanguageCommand('_executeCodeLensProvider', function (accessor, args) {
|
||||
|
||||
let { resource, itemResolveCount } = args;
|
||||
if (!(resource instanceof URI)) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const result: CodeLens[] = [];
|
||||
const disposables = new DisposableStore();
|
||||
return getCodeLensModel(model, CancellationToken.None).then(value => {
|
||||
|
||||
disposables.add(value);
|
||||
let resolve: Promise<any>[] = [];
|
||||
|
||||
for (const item of value.lenses) {
|
||||
if (typeof itemResolveCount === 'undefined' || Boolean(item.symbol.command)) {
|
||||
result.push(item.symbol);
|
||||
} else if (itemResolveCount-- > 0 && item.provider.resolveCodeLens) {
|
||||
resolve.push(Promise.resolve(item.provider.resolveCodeLens(model, item.symbol, CancellationToken.None)).then(symbol => result.push(symbol || item.symbol)));
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.all(resolve);
|
||||
|
||||
}).then(() => {
|
||||
return result;
|
||||
}).finally(() => {
|
||||
// make sure to return results, then (on next tick)
|
||||
// dispose the results
|
||||
setTimeout(() => disposables.dispose(), 100);
|
||||
});
|
||||
});
|
||||
476
lib/vscode/src/vs/editor/contrib/codelens/codelensController.ts
Normal file
476
lib/vscode/src/vs/editor/contrib/codelens/codelensController.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancelablePromise, RunOnceScheduler, createCancelablePromise, disposableTimeout } from 'vs/base/common/async';
|
||||
import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { StableEditorScrollState } from 'vs/editor/browser/core/editorState';
|
||||
import { ICodeEditor, MouseTargetType, IViewZoneChangeAccessor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution, ServicesAccessor, registerEditorAction, EditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
|
||||
import { CodeLensProviderRegistry, CodeLens, Command } from 'vs/editor/common/modes';
|
||||
import { CodeLensModel, getCodeLensModel, CodeLensItem } from 'vs/editor/contrib/codelens/codelens';
|
||||
import { CodeLensWidget, CodeLensHelper } from 'vs/editor/contrib/codelens/codelensWidget';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { ICodeLensCache } from 'vs/editor/contrib/codelens/codeLensCache';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { localize } from 'vs/nls';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry';
|
||||
|
||||
export class CodeLensContribution implements IEditorContribution {
|
||||
|
||||
static readonly ID: string = 'css.editor.codeLens';
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private readonly _localToDispose = new DisposableStore();
|
||||
private readonly _styleElement: HTMLStyleElement;
|
||||
private readonly _styleClassName: string;
|
||||
private readonly _lenses: CodeLensWidget[] = [];
|
||||
|
||||
private readonly _getCodeLensModelDelays = new LanguageFeatureRequestDelays(CodeLensProviderRegistry, 250, 2500);
|
||||
private _getCodeLensModelPromise: CancelablePromise<CodeLensModel> | undefined;
|
||||
private _oldCodeLensModels = new DisposableStore();
|
||||
private _currentCodeLensModel: CodeLensModel | undefined;
|
||||
private readonly _resolveCodeLensesDelays = new LanguageFeatureRequestDelays(CodeLensProviderRegistry, 250, 2500);
|
||||
private readonly _resolveCodeLensesScheduler = new RunOnceScheduler(() => this._resolveCodeLensesInViewport(), this._resolveCodeLensesDelays.min);
|
||||
private _resolveCodeLensesPromise: CancelablePromise<any> | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _editor: ICodeEditor,
|
||||
@ICommandService private readonly _commandService: ICommandService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@ICodeLensCache private readonly _codeLensCache: ICodeLensCache
|
||||
) {
|
||||
|
||||
this._disposables.add(this._editor.onDidChangeModel(() => this._onModelChange()));
|
||||
this._disposables.add(this._editor.onDidChangeModelLanguage(() => this._onModelChange()));
|
||||
this._disposables.add(this._editor.onDidChangeConfiguration((e) => {
|
||||
if (e.hasChanged(EditorOption.fontInfo)) {
|
||||
this._updateLensStyle();
|
||||
}
|
||||
if (e.hasChanged(EditorOption.codeLens)) {
|
||||
this._onModelChange();
|
||||
}
|
||||
}));
|
||||
this._disposables.add(CodeLensProviderRegistry.onDidChange(this._onModelChange, this));
|
||||
this._onModelChange();
|
||||
|
||||
this._styleClassName = '_' + hash(this._editor.getId()).toString(16);
|
||||
this._styleElement = dom.createStyleSheet(
|
||||
dom.isInShadowDOM(this._editor.getContainerDomNode())
|
||||
? this._editor.getContainerDomNode()
|
||||
: undefined
|
||||
);
|
||||
this._updateLensStyle();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._localDispose();
|
||||
this._disposables.dispose();
|
||||
this._oldCodeLensModels.dispose();
|
||||
this._currentCodeLensModel?.dispose();
|
||||
}
|
||||
|
||||
private _updateLensStyle(): void {
|
||||
const options = this._editor.getOptions();
|
||||
const fontInfo = options.get(EditorOption.fontInfo);
|
||||
const lineHeight = options.get(EditorOption.lineHeight);
|
||||
|
||||
|
||||
const height = Math.round(lineHeight * 1.1);
|
||||
const fontSize = Math.round(fontInfo.fontSize * 0.9);
|
||||
const newStyle = `
|
||||
.monaco-editor .codelens-decoration.${this._styleClassName} { height: ${height}px; line-height: ${lineHeight}px; font-size: ${fontSize}px; padding-right: ${Math.round(fontInfo.fontSize * 0.45)}px;}
|
||||
.monaco-editor .codelens-decoration.${this._styleClassName} > a > .codicon { line-height: ${lineHeight}px; font-size: ${fontSize}px; }
|
||||
`;
|
||||
this._styleElement.textContent = newStyle;
|
||||
}
|
||||
|
||||
private _localDispose(): void {
|
||||
this._getCodeLensModelPromise?.cancel();
|
||||
this._getCodeLensModelPromise = undefined;
|
||||
this._resolveCodeLensesPromise?.cancel();
|
||||
this._resolveCodeLensesPromise = undefined;
|
||||
this._localToDispose.clear();
|
||||
this._oldCodeLensModels.clear();
|
||||
this._currentCodeLensModel?.dispose();
|
||||
}
|
||||
|
||||
private _onModelChange(): void {
|
||||
|
||||
this._localDispose();
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._editor.getOption(EditorOption.codeLens)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cachedLenses = this._codeLensCache.get(model);
|
||||
if (cachedLenses) {
|
||||
this._renderCodeLensSymbols(cachedLenses);
|
||||
}
|
||||
|
||||
if (!CodeLensProviderRegistry.has(model)) {
|
||||
// no provider -> return but check with
|
||||
// cached lenses. they expire after 30 seconds
|
||||
if (cachedLenses) {
|
||||
this._localToDispose.add(disposableTimeout(() => {
|
||||
const cachedLensesNow = this._codeLensCache.get(model);
|
||||
if (cachedLenses === cachedLensesNow) {
|
||||
this._codeLensCache.delete(model);
|
||||
this._onModelChange();
|
||||
}
|
||||
}, 30 * 1000));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const provider of CodeLensProviderRegistry.all(model)) {
|
||||
if (typeof provider.onDidChange === 'function') {
|
||||
let registration = provider.onDidChange(() => scheduler.schedule());
|
||||
this._localToDispose.add(registration);
|
||||
}
|
||||
}
|
||||
|
||||
const scheduler = new RunOnceScheduler(() => {
|
||||
const t1 = Date.now();
|
||||
|
||||
this._getCodeLensModelPromise?.cancel();
|
||||
this._getCodeLensModelPromise = createCancelablePromise(token => getCodeLensModel(model, token));
|
||||
|
||||
this._getCodeLensModelPromise.then(result => {
|
||||
if (this._currentCodeLensModel) {
|
||||
this._oldCodeLensModels.add(this._currentCodeLensModel);
|
||||
}
|
||||
this._currentCodeLensModel = result;
|
||||
|
||||
// cache model to reduce flicker
|
||||
this._codeLensCache.put(model, result);
|
||||
|
||||
// update moving average
|
||||
const newDelay = this._getCodeLensModelDelays.update(model, Date.now() - t1);
|
||||
scheduler.delay = newDelay;
|
||||
|
||||
// render lenses
|
||||
this._renderCodeLensSymbols(result);
|
||||
this._resolveCodeLensesInViewportSoon();
|
||||
}, onUnexpectedError);
|
||||
|
||||
}, this._getCodeLensModelDelays.get(model));
|
||||
|
||||
this._localToDispose.add(scheduler);
|
||||
this._localToDispose.add(toDisposable(() => this._resolveCodeLensesScheduler.cancel()));
|
||||
this._localToDispose.add(this._editor.onDidChangeModelContent(() => {
|
||||
this._editor.changeDecorations(decorationsAccessor => {
|
||||
this._editor.changeViewZones(viewZonesAccessor => {
|
||||
let toDispose: CodeLensWidget[] = [];
|
||||
let lastLensLineNumber: number = -1;
|
||||
|
||||
this._lenses.forEach((lens) => {
|
||||
if (!lens.isValid() || lastLensLineNumber === lens.getLineNumber()) {
|
||||
// invalid -> lens collapsed, attach range doesn't exist anymore
|
||||
// line_number -> lenses should never be on the same line
|
||||
toDispose.push(lens);
|
||||
|
||||
} else {
|
||||
lens.update(viewZonesAccessor);
|
||||
lastLensLineNumber = lens.getLineNumber();
|
||||
}
|
||||
});
|
||||
|
||||
let helper = new CodeLensHelper();
|
||||
toDispose.forEach((l) => {
|
||||
l.dispose(helper, viewZonesAccessor);
|
||||
this._lenses.splice(this._lenses.indexOf(l), 1);
|
||||
});
|
||||
helper.commit(decorationsAccessor);
|
||||
});
|
||||
});
|
||||
|
||||
// Compute new `visible` code lenses
|
||||
this._resolveCodeLensesInViewportSoon();
|
||||
// Ask for all references again
|
||||
scheduler.schedule();
|
||||
}));
|
||||
this._localToDispose.add(this._editor.onDidScrollChange(e => {
|
||||
if (e.scrollTopChanged && this._lenses.length > 0) {
|
||||
this._resolveCodeLensesInViewportSoon();
|
||||
}
|
||||
}));
|
||||
this._localToDispose.add(this._editor.onDidLayoutChange(() => {
|
||||
this._resolveCodeLensesInViewportSoon();
|
||||
}));
|
||||
this._localToDispose.add(toDisposable(() => {
|
||||
if (this._editor.getModel()) {
|
||||
const scrollState = StableEditorScrollState.capture(this._editor);
|
||||
this._editor.changeDecorations(decorationsAccessor => {
|
||||
this._editor.changeViewZones(viewZonesAccessor => {
|
||||
this._disposeAllLenses(decorationsAccessor, viewZonesAccessor);
|
||||
});
|
||||
});
|
||||
scrollState.restore(this._editor);
|
||||
} else {
|
||||
// No accessors available
|
||||
this._disposeAllLenses(undefined, undefined);
|
||||
}
|
||||
}));
|
||||
this._localToDispose.add(this._editor.onMouseDown(e => {
|
||||
if (e.target.type !== MouseTargetType.CONTENT_WIDGET) {
|
||||
return;
|
||||
}
|
||||
let target = e.target.element;
|
||||
if (target?.tagName === 'SPAN') {
|
||||
target = target.parentElement;
|
||||
}
|
||||
if (target?.tagName === 'A') {
|
||||
for (const lens of this._lenses) {
|
||||
let command = lens.getCommand(target as HTMLLinkElement);
|
||||
if (command) {
|
||||
this._commandService.executeCommand(command.id, ...(command.arguments || [])).catch(err => this._notificationService.error(err));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
scheduler.schedule();
|
||||
}
|
||||
|
||||
private _disposeAllLenses(decChangeAccessor: IModelDecorationsChangeAccessor | undefined, viewZoneChangeAccessor: IViewZoneChangeAccessor | undefined): void {
|
||||
const helper = new CodeLensHelper();
|
||||
for (const lens of this._lenses) {
|
||||
lens.dispose(helper, viewZoneChangeAccessor);
|
||||
}
|
||||
if (decChangeAccessor) {
|
||||
helper.commit(decChangeAccessor);
|
||||
}
|
||||
this._lenses.length = 0;
|
||||
}
|
||||
|
||||
private _renderCodeLensSymbols(symbols: CodeLensModel): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let maxLineNumber = this._editor.getModel().getLineCount();
|
||||
let groups: CodeLensItem[][] = [];
|
||||
let lastGroup: CodeLensItem[] | undefined;
|
||||
|
||||
for (let symbol of symbols.lenses) {
|
||||
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 scrollState = StableEditorScrollState.capture(this._editor);
|
||||
|
||||
this._editor.changeDecorations(decorationsAccessor => {
|
||||
this._editor.changeViewZones(viewZoneAccessor => {
|
||||
|
||||
const helper = new CodeLensHelper();
|
||||
let codeLensIndex = 0;
|
||||
let groupsIndex = 0;
|
||||
|
||||
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, viewZoneAccessor);
|
||||
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 CodeLensWidget(groups[groupsIndex], <IActiveCodeEditor>this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._resolveCodeLensesInViewportSoon()));
|
||||
codeLensIndex++;
|
||||
groupsIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete extra code lenses
|
||||
while (codeLensIndex < this._lenses.length) {
|
||||
this._lenses[codeLensIndex].dispose(helper, viewZoneAccessor);
|
||||
this._lenses.splice(codeLensIndex, 1);
|
||||
}
|
||||
|
||||
// Create extra symbols
|
||||
while (groupsIndex < groups.length) {
|
||||
this._lenses.push(new CodeLensWidget(groups[groupsIndex], <IActiveCodeEditor>this._editor, this._styleClassName, helper, viewZoneAccessor, () => this._resolveCodeLensesInViewportSoon()));
|
||||
groupsIndex++;
|
||||
}
|
||||
|
||||
helper.commit(decorationsAccessor);
|
||||
});
|
||||
});
|
||||
|
||||
scrollState.restore(this._editor);
|
||||
}
|
||||
|
||||
private _resolveCodeLensesInViewportSoon(): void {
|
||||
const model = this._editor.getModel();
|
||||
if (model) {
|
||||
this._resolveCodeLensesScheduler.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveCodeLensesInViewport(): void {
|
||||
|
||||
this._resolveCodeLensesPromise?.cancel();
|
||||
this._resolveCodeLensesPromise = undefined;
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toResolve: CodeLensItem[][] = [];
|
||||
const lenses: CodeLensWidget[] = [];
|
||||
this._lenses.forEach((lens) => {
|
||||
const request = lens.computeIfNecessary(model);
|
||||
if (request) {
|
||||
toResolve.push(request);
|
||||
lenses.push(lens);
|
||||
}
|
||||
});
|
||||
|
||||
if (toResolve.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const t1 = Date.now();
|
||||
|
||||
const resolvePromise = createCancelablePromise(token => {
|
||||
|
||||
const promises = toResolve.map((request, i) => {
|
||||
|
||||
const resolvedSymbols = new Array<CodeLens | undefined | null>(request.length);
|
||||
const promises = request.map((request, i) => {
|
||||
if (!request.symbol.command && typeof request.provider.resolveCodeLens === 'function') {
|
||||
return Promise.resolve(request.provider.resolveCodeLens(model, request.symbol, token)).then(symbol => {
|
||||
resolvedSymbols[i] = symbol;
|
||||
}, onUnexpectedExternalError);
|
||||
} else {
|
||||
resolvedSymbols[i] = request.symbol;
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
if (!token.isCancellationRequested && !lenses[i].isDisposed()) {
|
||||
lenses[i].updateCommands(resolvedSymbols);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
this._resolveCodeLensesPromise = resolvePromise;
|
||||
|
||||
this._resolveCodeLensesPromise.then(() => {
|
||||
|
||||
// update moving average
|
||||
const newDelay = this._resolveCodeLensesDelays.update(model, Date.now() - t1);
|
||||
this._resolveCodeLensesScheduler.delay = newDelay;
|
||||
|
||||
if (this._currentCodeLensModel) { // update the cached state with new resolved items
|
||||
this._codeLensCache.put(model, this._currentCodeLensModel);
|
||||
}
|
||||
this._oldCodeLensModels.clear(); // dispose old models once we have updated the UI with the current model
|
||||
if (resolvePromise === this._resolveCodeLensesPromise) {
|
||||
this._resolveCodeLensesPromise = undefined;
|
||||
}
|
||||
}, err => {
|
||||
onUnexpectedError(err); // can also be cancellation!
|
||||
if (resolvePromise === this._resolveCodeLensesPromise) {
|
||||
this._resolveCodeLensesPromise = undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getLenses(): readonly CodeLensWidget[] {
|
||||
return this._lenses;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(CodeLensContribution.ID, CodeLensContribution);
|
||||
|
||||
registerEditorAction(class ShowLensesInCurrentLine extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'codelens.showLensesInCurrentLine',
|
||||
precondition: EditorContextKeys.hasCodeLensProvider,
|
||||
label: localize('showLensOnLine', "Show CodeLens Commands For Current Line"),
|
||||
alias: 'Show CodeLens Commands For Current Line',
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
const commandService = accessor.get(ICommandService);
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
|
||||
const lineNumber = editor.getSelection().positionLineNumber;
|
||||
const codelensController = editor.getContribution<CodeLensContribution>(CodeLensContribution.ID);
|
||||
const items: { label: string, command: Command }[] = [];
|
||||
|
||||
for (let lens of codelensController.getLenses()) {
|
||||
if (lens.getLineNumber() === lineNumber) {
|
||||
for (let item of lens.getItems()) {
|
||||
const { command } = item.symbol;
|
||||
if (command) {
|
||||
items.push({
|
||||
label: command.title,
|
||||
command: command
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
// We dont want an empty picker
|
||||
return;
|
||||
}
|
||||
|
||||
const item = await quickInputService.pick(items, { canPickMany: false });
|
||||
if (!item) {
|
||||
// Nothing picked
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await commandService.executeCommand(item.command.id, ...(item.command.arguments || []));
|
||||
} catch (err) {
|
||||
notificationService.error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
46
lib/vscode/src/vs/editor/contrib/codelens/codelensWidget.css
Normal file
46
lib/vscode/src/vs/editor/contrib/codelens/codelensWidget.css
Normal file
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .codelens-decoration {
|
||||
overflow: hidden;
|
||||
display: inline-block;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > span,
|
||||
.monaco-editor .codelens-decoration > a {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
white-space: nowrap;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > a:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration .codicon {
|
||||
vertical-align: middle;
|
||||
color: currentColor !important;
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration > a:hover .codicon::before {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
0% { opacity: 0; visibility: visible;}
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
.monaco-editor .codelens-decoration.fadein {
|
||||
animation: fadein 0.1s linear;
|
||||
}
|
||||
352
lib/vscode/src/vs/editor/contrib/codelens/codelensWidget.ts
Normal file
352
lib/vscode/src/vs/editor/contrib/codelens/codelensWidget.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./codelensWidget';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IViewZone, IContentWidget, IActiveCodeEditor, IContentWidgetPosition, ContentWidgetPositionPreference, IViewZoneChangeAccessor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { Command, CodeLens } from 'vs/editor/common/modes';
|
||||
import { editorCodeLensForeground } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { CodeLensItem } from 'vs/editor/contrib/codelens/codelens';
|
||||
import { editorActiveLinkForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { renderCodicons } from 'vs/base/browser/codicons';
|
||||
|
||||
class CodeLensViewZone implements IViewZone {
|
||||
|
||||
readonly heightInLines: number;
|
||||
readonly suppressMouseDown: boolean;
|
||||
readonly domNode: HTMLElement;
|
||||
|
||||
afterLineNumber: number;
|
||||
|
||||
private _lastHeight?: number;
|
||||
private readonly _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 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 _editor: IActiveCodeEditor;
|
||||
private readonly _commands = new Map<string, Command>();
|
||||
|
||||
private _widgetPosition?: IContentWidgetPosition;
|
||||
private _isEmpty: boolean = true;
|
||||
|
||||
constructor(
|
||||
editor: IActiveCodeEditor,
|
||||
className: string,
|
||||
line: number,
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._id = `codelens.widget-${(CodeLensContentWidget._idPool++)}`;
|
||||
|
||||
this.updatePosition(line);
|
||||
|
||||
this._domNode = document.createElement('span');
|
||||
this._domNode.className = `codelens-decoration ${className}`;
|
||||
}
|
||||
|
||||
withCommands(lenses: Array<CodeLens | undefined | null>, animate: boolean): void {
|
||||
this._commands.clear();
|
||||
|
||||
let children: HTMLElement[] = [];
|
||||
let hasSymbol = false;
|
||||
for (let i = 0; i < lenses.length; i++) {
|
||||
const lens = lenses[i];
|
||||
if (!lens) {
|
||||
continue;
|
||||
}
|
||||
hasSymbol = true;
|
||||
if (lens.command) {
|
||||
const title = renderCodicons(lens.command.title.trim());
|
||||
if (lens.command.id) {
|
||||
children.push(dom.$('a', { id: String(i) }, ...title));
|
||||
this._commands.set(String(i), lens.command);
|
||||
} else {
|
||||
children.push(dom.$('span', undefined, ...title));
|
||||
}
|
||||
if (i + 1 < lenses.length) {
|
||||
children.push(dom.$('span', undefined, '\u00a0|\u00a0'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSymbol) {
|
||||
// symbols but no commands
|
||||
dom.reset(this._domNode, dom.$('span', undefined, 'no commands'));
|
||||
|
||||
} else {
|
||||
// symbols and commands
|
||||
dom.reset(this._domNode, ...children);
|
||||
if (this._isEmpty && animate) {
|
||||
this._domNode.classList.add('fadein');
|
||||
}
|
||||
this._isEmpty = false;
|
||||
}
|
||||
}
|
||||
|
||||
getCommand(link: HTMLLinkElement): Command | undefined {
|
||||
return link.parentElement === this._domNode
|
||||
? this._commands.get(link.id)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
getId(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
getDomNode(): HTMLElement {
|
||||
return this._domNode;
|
||||
}
|
||||
|
||||
updatePosition(line: number): void {
|
||||
const column = this._editor.getModel().getLineFirstNonWhitespaceColumn(line);
|
||||
this._widgetPosition = {
|
||||
position: { lineNumber: line, column: column },
|
||||
preference: [ContentWidgetPositionPreference.ABOVE]
|
||||
};
|
||||
}
|
||||
|
||||
getPosition(): IContentWidgetPosition | null {
|
||||
return this._widgetPosition || null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IDecorationIdCallback {
|
||||
(decorationId: string): void;
|
||||
}
|
||||
|
||||
export class CodeLensHelper {
|
||||
|
||||
private readonly _removeDecorations: string[];
|
||||
private readonly _addDecorations: IModelDeltaDecoration[];
|
||||
private readonly _addDecorationsCallbacks: IDecorationIdCallback[];
|
||||
|
||||
constructor() {
|
||||
this._removeDecorations = [];
|
||||
this._addDecorations = [];
|
||||
this._addDecorationsCallbacks = [];
|
||||
}
|
||||
|
||||
addDecoration(decoration: IModelDeltaDecoration, callback: IDecorationIdCallback): void {
|
||||
this._addDecorations.push(decoration);
|
||||
this._addDecorationsCallbacks.push(callback);
|
||||
}
|
||||
|
||||
removeDecoration(decorationId: string): void {
|
||||
this._removeDecorations.push(decorationId);
|
||||
}
|
||||
|
||||
commit(changeAccessor: IModelDecorationsChangeAccessor): void {
|
||||
let resultingDecorations = changeAccessor.deltaDecorations(this._removeDecorations, this._addDecorations);
|
||||
for (let i = 0, len = resultingDecorations.length; i < len; i++) {
|
||||
this._addDecorationsCallbacks[i](resultingDecorations[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class CodeLensWidget {
|
||||
|
||||
private readonly _editor: IActiveCodeEditor;
|
||||
private readonly _className: string;
|
||||
private readonly _viewZone!: CodeLensViewZone;
|
||||
private readonly _viewZoneId!: string;
|
||||
|
||||
private _contentWidget?: CodeLensContentWidget;
|
||||
private _decorationIds: string[];
|
||||
private _data: CodeLensItem[];
|
||||
private _isDisposed: boolean = false;
|
||||
|
||||
constructor(
|
||||
data: CodeLensItem[],
|
||||
editor: IActiveCodeEditor,
|
||||
className: string,
|
||||
helper: CodeLensHelper,
|
||||
viewZoneChangeAccessor: IViewZoneChangeAccessor,
|
||||
updateCallback: Function
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._className = className;
|
||||
this._data = data;
|
||||
|
||||
// create combined range, track all ranges with decorations,
|
||||
// check if there is already something to render
|
||||
this._decorationIds = [];
|
||||
let range: Range | undefined;
|
||||
let lenses: CodeLens[] = [];
|
||||
|
||||
this._data.forEach((codeLensData, i) => {
|
||||
|
||||
if (codeLensData.symbol.command) {
|
||||
lenses.push(codeLensData.symbol);
|
||||
}
|
||||
|
||||
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._viewZone = new CodeLensViewZone(range!.startLineNumber - 1, updateCallback);
|
||||
this._viewZoneId = viewZoneChangeAccessor.addZone(this._viewZone);
|
||||
|
||||
if (lenses.length > 0) {
|
||||
this._createContentWidgetIfNecessary();
|
||||
this._contentWidget!.withCommands(lenses, false);
|
||||
}
|
||||
}
|
||||
|
||||
private _createContentWidgetIfNecessary(): void {
|
||||
if (!this._contentWidget) {
|
||||
this._contentWidget = new CodeLensContentWidget(this._editor, this._className, this._viewZone.afterLineNumber + 1);
|
||||
this._editor.addContentWidget(this._contentWidget!);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(helper: CodeLensHelper, viewZoneChangeAccessor?: IViewZoneChangeAccessor): void {
|
||||
this._decorationIds.forEach(helper.removeDecoration, helper);
|
||||
this._decorationIds = [];
|
||||
if (viewZoneChangeAccessor) {
|
||||
viewZoneChangeAccessor.removeZone(this._viewZoneId);
|
||||
}
|
||||
if (this._contentWidget) {
|
||||
this._editor.removeContentWidget(this._contentWidget);
|
||||
this._contentWidget = undefined;
|
||||
}
|
||||
this._isDisposed = true;
|
||||
}
|
||||
|
||||
isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
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: CodeLensItem[], helper: CodeLensHelper): void {
|
||||
this._decorationIds.forEach(helper.removeDecoration, helper);
|
||||
this._decorationIds = [];
|
||||
this._data = data;
|
||||
this._data.forEach((codeLensData, i) => {
|
||||
helper.addDecoration({
|
||||
range: codeLensData.symbol.range,
|
||||
options: ModelDecorationOptions.EMPTY
|
||||
}, id => this._decorationIds[i] = id);
|
||||
});
|
||||
}
|
||||
|
||||
computeIfNecessary(model: ITextModel): CodeLensItem[] | null {
|
||||
if (!this._viewZone.domNode.hasAttribute('monaco-visible-view-zone')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read editor current state
|
||||
for (let i = 0; i < this._decorationIds.length; i++) {
|
||||
const range = model.getDecorationRange(this._decorationIds[i]);
|
||||
if (range) {
|
||||
this._data[i].symbol.range = range;
|
||||
}
|
||||
}
|
||||
return this._data;
|
||||
}
|
||||
|
||||
updateCommands(symbols: Array<CodeLens | undefined | null>): void {
|
||||
|
||||
this._createContentWidgetIfNecessary();
|
||||
this._contentWidget!.withCommands(symbols, true);
|
||||
|
||||
for (let i = 0; i < this._data.length; i++) {
|
||||
const resolved = symbols[i];
|
||||
if (resolved) {
|
||||
const { symbol } = this._data[i];
|
||||
symbol.command = resolved.command || symbol.command;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCommand(link: HTMLLinkElement): Command | undefined {
|
||||
return this._contentWidget?.getCommand(link);
|
||||
}
|
||||
|
||||
getLineNumber(): number {
|
||||
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
|
||||
if (range) {
|
||||
return range.startLineNumber;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
update(viewZoneChangeAccessor: IViewZoneChangeAccessor): void {
|
||||
if (this.isValid()) {
|
||||
const range = this._editor.getModel().getDecorationRange(this._decorationIds[0]);
|
||||
if (range) {
|
||||
this._viewZone.afterLineNumber = range.startLineNumber - 1;
|
||||
viewZoneChangeAccessor.layoutZone(this._viewZoneId);
|
||||
|
||||
if (this._contentWidget) {
|
||||
this._contentWidget.updatePosition(range.startLineNumber);
|
||||
this._editor.layoutContentWidget(this._contentWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getItems(): CodeLensItem[] {
|
||||
return this._data;
|
||||
}
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const codeLensForeground = theme.getColor(editorCodeLensForeground);
|
||||
if (codeLensForeground) {
|
||||
collector.addRule(`.monaco-editor .codelens-decoration { color: ${codeLensForeground}; }`);
|
||||
collector.addRule(`.monaco-editor .codelens-decoration .codicon { color: ${codeLensForeground}; }`);
|
||||
}
|
||||
const activeLinkForeground = theme.getColor(editorActiveLinkForeground);
|
||||
if (activeLinkForeground) {
|
||||
collector.addRule(`.monaco-editor .codelens-decoration > a:hover { color: ${activeLinkForeground} !important; }`);
|
||||
collector.addRule(`.monaco-editor .codelens-decoration > a:hover .codicon { color: ${activeLinkForeground} !important; }`);
|
||||
}
|
||||
});
|
||||
91
lib/vscode/src/vs/editor/contrib/colorPicker/color.ts
Normal file
91
lib/vscode/src/vs/editor/contrib/colorPicker/color.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { illegalArgument } from 'vs/base/common/errors';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { registerLanguageCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { ColorProviderRegistry, DocumentColorProvider, IColorInformation, IColorPresentation } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
|
||||
|
||||
export interface IColorData {
|
||||
colorInfo: IColorInformation;
|
||||
provider: DocumentColorProvider;
|
||||
}
|
||||
|
||||
export function getColors(model: ITextModel, token: CancellationToken): Promise<IColorData[]> {
|
||||
const colors: IColorData[] = [];
|
||||
const providers = ColorProviderRegistry.ordered(model).reverse();
|
||||
const promises = providers.map(provider => Promise.resolve(provider.provideDocumentColors(model, token)).then(result => {
|
||||
if (Array.isArray(result)) {
|
||||
for (let colorInfo of result) {
|
||||
colors.push({ colorInfo, provider });
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
return Promise.all(promises).then(() => colors);
|
||||
}
|
||||
|
||||
export function getColorPresentations(model: ITextModel, colorInfo: IColorInformation, provider: DocumentColorProvider, token: CancellationToken): Promise<IColorPresentation[] | null | undefined> {
|
||||
return Promise.resolve(provider.provideColorPresentations(model, colorInfo, token));
|
||||
}
|
||||
|
||||
registerLanguageCommand('_executeDocumentColorProvider', function (accessor, args) {
|
||||
|
||||
const { resource } = args;
|
||||
if (!(resource instanceof URI)) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const rawCIs: { range: IRange, color: [number, number, number, number] }[] = [];
|
||||
const providers = ColorProviderRegistry.ordered(model).reverse();
|
||||
const promises = providers.map(provider => Promise.resolve(provider.provideDocumentColors(model, CancellationToken.None)).then(result => {
|
||||
if (Array.isArray(result)) {
|
||||
for (let ci of result) {
|
||||
rawCIs.push({ range: ci.range, color: [ci.color.red, ci.color.green, ci.color.blue, ci.color.alpha] });
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
return Promise.all(promises).then(() => rawCIs);
|
||||
});
|
||||
|
||||
|
||||
registerLanguageCommand('_executeColorPresentationProvider', function (accessor, args) {
|
||||
|
||||
const { resource, color, range } = args;
|
||||
if (!(resource instanceof URI) || !Array.isArray(color) || color.length !== 4 || !Range.isIRange(range)) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
const [red, green, blue, alpha] = color;
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument();
|
||||
}
|
||||
|
||||
const colorInfo = {
|
||||
range,
|
||||
color: { red, green, blue, alpha }
|
||||
};
|
||||
|
||||
const presentations: IColorPresentation[] = [];
|
||||
const providers = ColorProviderRegistry.ordered(model).reverse();
|
||||
const promises = providers.map(provider => Promise.resolve(provider.provideColorPresentations(model, colorInfo, CancellationToken.None)).then(result => {
|
||||
if (Array.isArray(result)) {
|
||||
presentations.push(...result);
|
||||
}
|
||||
}));
|
||||
return Promise.all(promises).then(() => presentations);
|
||||
});
|
||||
246
lib/vscode/src/vs/editor/contrib/colorPicker/colorDetector.ts
Normal file
246
lib/vscode/src/vs/editor/contrib/colorPicker/colorDetector.ts
Normal file
@@ -0,0 +1,246 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancelablePromise, TimeoutTimer, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { RGBA } from 'vs/base/common/color';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { IModelDeltaDecoration } from 'vs/editor/common/model';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { ColorProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { IColorData, getColors } from 'vs/editor/contrib/colorPicker/color';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
const MAX_DECORATORS = 500;
|
||||
|
||||
export class ColorDetector extends Disposable implements IEditorContribution {
|
||||
|
||||
public static readonly ID: string = 'editor.contrib.colorDetector';
|
||||
|
||||
static readonly RECOMPUTE_TIME = 1000; // ms
|
||||
|
||||
private readonly _localToDispose = this._register(new DisposableStore());
|
||||
private _computePromise: CancelablePromise<IColorData[]> | null;
|
||||
private _timeoutTimer: TimeoutTimer | null;
|
||||
|
||||
private _decorationsIds: string[] = [];
|
||||
private _colorDatas = new Map<string, IColorData>();
|
||||
|
||||
private _colorDecoratorIds: string[] = [];
|
||||
private readonly _decorationsTypes = new Set<string>();
|
||||
|
||||
private _isEnabled: boolean;
|
||||
|
||||
constructor(private readonly _editor: ICodeEditor,
|
||||
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService
|
||||
) {
|
||||
super();
|
||||
this._register(_editor.onDidChangeModel((e) => {
|
||||
this._isEnabled = this.isEnabled();
|
||||
this.onModelChanged();
|
||||
}));
|
||||
this._register(_editor.onDidChangeModelLanguage((e) => this.onModelChanged()));
|
||||
this._register(ColorProviderRegistry.onDidChange((e) => this.onModelChanged()));
|
||||
this._register(_editor.onDidChangeConfiguration((e) => {
|
||||
let prevIsEnabled = this._isEnabled;
|
||||
this._isEnabled = this.isEnabled();
|
||||
if (prevIsEnabled !== this._isEnabled) {
|
||||
if (this._isEnabled) {
|
||||
this.onModelChanged();
|
||||
} else {
|
||||
this.removeAllDecorations();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
this._timeoutTimer = 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
|
||||
const deprecatedConfig = this._configurationService.getValue<{}>(languageId.language);
|
||||
if (deprecatedConfig) {
|
||||
const colorDecorators = (deprecatedConfig as any)['colorDecorators']; // deprecatedConfig.valueOf('.colorDecorators.enable');
|
||||
if (colorDecorators && colorDecorators['enable'] !== undefined && !colorDecorators['enable']) {
|
||||
return colorDecorators['enable'];
|
||||
}
|
||||
}
|
||||
|
||||
return this._editor.getOption(EditorOption.colorDecorators);
|
||||
}
|
||||
|
||||
static get(editor: ICodeEditor): ColorDetector {
|
||||
return editor.getContribution<ColorDetector>(this.ID);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
this.removeAllDecorations();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private onModelChanged(): void {
|
||||
this.stop();
|
||||
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
const model = this._editor.getModel();
|
||||
|
||||
if (!model || !ColorProviderRegistry.has(model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._localToDispose.add(this._editor.onDidChangeModelContent((e) => {
|
||||
if (!this._timeoutTimer) {
|
||||
this._timeoutTimer = new TimeoutTimer();
|
||||
this._timeoutTimer.cancelAndSet(() => {
|
||||
this._timeoutTimer = null;
|
||||
this.beginCompute();
|
||||
}, ColorDetector.RECOMPUTE_TIME);
|
||||
}
|
||||
}));
|
||||
this.beginCompute();
|
||||
}
|
||||
|
||||
private beginCompute(): void {
|
||||
this._computePromise = createCancelablePromise(token => {
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
return getColors(model, token);
|
||||
});
|
||||
this._computePromise.then((colorInfos) => {
|
||||
this.updateDecorations(colorInfos);
|
||||
this.updateColorDecorators(colorInfos);
|
||||
this._computePromise = null;
|
||||
}, onUnexpectedError);
|
||||
}
|
||||
|
||||
private stop(): void {
|
||||
if (this._timeoutTimer) {
|
||||
this._timeoutTimer.cancel();
|
||||
this._timeoutTimer = null;
|
||||
}
|
||||
if (this._computePromise) {
|
||||
this._computePromise.cancel();
|
||||
this._computePromise = null;
|
||||
}
|
||||
this._localToDispose.clear();
|
||||
}
|
||||
|
||||
private updateDecorations(colorDatas: IColorData[]): void {
|
||||
const decorations = colorDatas.map(c => ({
|
||||
range: {
|
||||
startLineNumber: c.colorInfo.range.startLineNumber,
|
||||
startColumn: c.colorInfo.range.startColumn,
|
||||
endLineNumber: c.colorInfo.range.endLineNumber,
|
||||
endColumn: c.colorInfo.range.endColumn
|
||||
},
|
||||
options: ModelDecorationOptions.EMPTY
|
||||
}));
|
||||
|
||||
this._decorationsIds = this._editor.deltaDecorations(this._decorationsIds, decorations);
|
||||
|
||||
this._colorDatas = new Map<string, IColorData>();
|
||||
this._decorationsIds.forEach((id, i) => this._colorDatas.set(id, colorDatas[i]));
|
||||
}
|
||||
|
||||
private updateColorDecorators(colorData: IColorData[]): void {
|
||||
let decorations: IModelDeltaDecoration[] = [];
|
||||
let newDecorationsTypes: { [key: string]: boolean } = {};
|
||||
|
||||
for (let i = 0; i < colorData.length && decorations.length < MAX_DECORATORS; i++) {
|
||||
const { red, green, blue, alpha } = colorData[i].colorInfo.color;
|
||||
const rgba = new RGBA(Math.round(red * 255), Math.round(green * 255), Math.round(blue * 255), alpha);
|
||||
let subKey = hash(`rgba(${rgba.r},${rgba.g},${rgba.b},${rgba.a})`).toString(16);
|
||||
let color = `rgba(${rgba.r}, ${rgba.g}, ${rgba.b}, ${rgba.a})`;
|
||||
let key = 'colorBox-' + subKey;
|
||||
|
||||
if (!this._decorationsTypes.has(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'
|
||||
}
|
||||
}
|
||||
}, undefined, this._editor);
|
||||
}
|
||||
|
||||
newDecorationsTypes[key] = true;
|
||||
decorations.push({
|
||||
range: {
|
||||
startLineNumber: colorData[i].colorInfo.range.startLineNumber,
|
||||
startColumn: colorData[i].colorInfo.range.startColumn,
|
||||
endLineNumber: colorData[i].colorInfo.range.endLineNumber,
|
||||
endColumn: colorData[i].colorInfo.range.endColumn
|
||||
},
|
||||
options: this._codeEditorService.resolveDecorationOptions(key, true)
|
||||
});
|
||||
}
|
||||
|
||||
this._decorationsTypes.forEach(subType => {
|
||||
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, []);
|
||||
|
||||
this._decorationsTypes.forEach(subType => {
|
||||
this._codeEditorService.removeDecorationType(subType);
|
||||
});
|
||||
}
|
||||
|
||||
getColorData(position: Position): IColorData | null {
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decorations = model
|
||||
.getDecorationsInRange(Range.fromPositions(position, position))
|
||||
.filter(d => this._colorDatas.has(d.id));
|
||||
|
||||
if (decorations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._colorDatas.get(decorations[0].id)!;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(ColorDetector.ID, ColorDetector);
|
||||
120
lib/vscode/src/vs/editor/contrib/colorPicker/colorPicker.css
Normal file
120
lib/vscode/src/vs/editor/contrib/colorPicker/colorPicker.css
Normal file
@@ -0,0 +1,120 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.colorpicker-widget {
|
||||
height: 190px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.monaco-editor .colorpicker-hover: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: 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: grab;
|
||||
background: url('images/opacity-background.png');
|
||||
background-size: 9px 9px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.colorpicker-body .strip.grabbing {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.colorpicker-body .slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -2px;
|
||||
width: calc(100% + 4px);
|
||||
height: 4px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid rgba(255, 255, 255, 0.71);
|
||||
box-shadow: 0px 0px 1px rgba(0, 0, 0, 0.85);
|
||||
}
|
||||
|
||||
.colorpicker-body .strip .overlay {
|
||||
height: 150px;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IColorPresentation } from 'vs/editor/common/modes';
|
||||
|
||||
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._onDidChangeColor.fire(color);
|
||||
}
|
||||
|
||||
get presentation(): IColorPresentation { return this.colorPresentations[this.presentationIndex]; }
|
||||
|
||||
private _colorPresentations: IColorPresentation[];
|
||||
|
||||
get colorPresentations(): IColorPresentation[] {
|
||||
return this._colorPresentations;
|
||||
}
|
||||
|
||||
set colorPresentations(colorPresentations: IColorPresentation[]) {
|
||||
this._colorPresentations = colorPresentations;
|
||||
if (this.presentationIndex > colorPresentations.length - 1) {
|
||||
this.presentationIndex = 0;
|
||||
}
|
||||
this._onDidChangePresentation.fire(this.presentation);
|
||||
}
|
||||
|
||||
private readonly _onColorFlushed = new Emitter<Color>();
|
||||
readonly onColorFlushed: Event<Color> = this._onColorFlushed.event;
|
||||
|
||||
private readonly _onDidChangeColor = new Emitter<Color>();
|
||||
readonly onDidChangeColor: Event<Color> = this._onDidChangeColor.event;
|
||||
|
||||
private readonly _onDidChangePresentation = new Emitter<IColorPresentation>();
|
||||
readonly onDidChangePresentation: Event<IColorPresentation> = this._onDidChangePresentation.event;
|
||||
|
||||
constructor(color: Color, availableColorPresentations: IColorPresentation[], private presentationIndex: number) {
|
||||
this.originalColor = color;
|
||||
this._color = color;
|
||||
this._colorPresentations = availableColorPresentations;
|
||||
}
|
||||
|
||||
selectNextColorPresentation(): void {
|
||||
this.presentationIndex = (this.presentationIndex + 1) % this.colorPresentations.length;
|
||||
this.flushColor();
|
||||
this._onDidChangePresentation.fire(this.presentation);
|
||||
}
|
||||
|
||||
guessColorPresentation(color: Color, originalText: string): void {
|
||||
for (let i = 0; i < this.colorPresentations.length; i++) {
|
||||
if (originalText.toLowerCase() === this.colorPresentations[i].label) {
|
||||
this.presentationIndex = i;
|
||||
this._onDidChangePresentation.fire(this.presentation);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flushColor(): void {
|
||||
this._onColorFlushed.fire(this._color);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./colorPicker';
|
||||
import { onDidChangeZoomLevel } from 'vs/base/browser/browser';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { Color, HSVA, RGBA } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ColorPickerModel } from 'vs/editor/contrib/colorPicker/colorPickerModel';
|
||||
import { editorHoverBackground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class ColorPickerHeader extends Disposable {
|
||||
|
||||
private readonly domNode: HTMLElement;
|
||||
private readonly pickedColorNode: HTMLElement;
|
||||
private backgroundColor: Color;
|
||||
|
||||
constructor(container: HTMLElement, private readonly model: ColorPickerModel, themeService: IThemeService) {
|
||||
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.backgroundColor = themeService.getColorTheme().getColor(editorHoverBackground) || Color.white;
|
||||
this._register(registerThemingParticipant((theme, collector) => {
|
||||
this.backgroundColor = theme.getColor(editorHoverBackground) || Color.white;
|
||||
}));
|
||||
|
||||
this._register(dom.addDisposableListener(this.pickedColorNode, dom.EventType.CLICK, () => this.model.selectNextColorPresentation()));
|
||||
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.onDidChangePresentation(this.onDidChangePresentation, this));
|
||||
this.pickedColorNode.style.backgroundColor = Color.Format.CSS.format(model.color) || '';
|
||||
this.pickedColorNode.classList.toggle('light', model.color.rgba.a < 0.5 ? this.backgroundColor.isLighter() : model.color.isLighter());
|
||||
}
|
||||
|
||||
private onDidChangeColor(color: Color): void {
|
||||
this.pickedColorNode.style.backgroundColor = Color.Format.CSS.format(color) || '';
|
||||
this.pickedColorNode.classList.toggle('light', color.rgba.a < 0.5 ? this.backgroundColor.isLighter() : color.isLighter());
|
||||
this.onDidChangePresentation();
|
||||
}
|
||||
|
||||
private onDidChangePresentation(): void {
|
||||
this.pickedColorNode.textContent = this.model.presentation ? this.model.presentation.label : '';
|
||||
}
|
||||
}
|
||||
|
||||
export class ColorPickerBody extends Disposable {
|
||||
|
||||
private readonly domNode: HTMLElement;
|
||||
private readonly saturationBox: SaturationBox;
|
||||
private readonly hueStrip: Strip;
|
||||
private readonly opacityStrip: Strip;
|
||||
|
||||
constructor(container: HTMLElement, private readonly 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 readonly domNode: HTMLElement;
|
||||
private readonly selection: HTMLElement;
|
||||
private readonly canvas: HTMLCanvasElement;
|
||||
private width!: number;
|
||||
private height!: number;
|
||||
|
||||
private monitor: GlobalMouseMoveMonitor<IStandardMouseMoveEventData> | null;
|
||||
private readonly _onDidChange = new Emitter<{ s: number, v: number }>();
|
||||
readonly onDidChange: Event<{ s: number, v: number }> = this._onDidChange.event;
|
||||
|
||||
private readonly _onColorFlushed = new Emitter<void>();
|
||||
readonly onColorFlushed: Event<void> = this._onColorFlushed.event;
|
||||
|
||||
constructor(container: HTMLElement, private readonly 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.addDisposableGenericMouseDownListner(this.domNode, 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(<HTMLElement>e.target, e.buttons, standardMouseMoveMerger, event => this.onDidChangePosition(event.posx - origin.left, event.posy - origin.top), () => null);
|
||||
|
||||
const mouseUpListener = dom.addDisposableGenericMouseUpListner(document, () => {
|
||||
this._onColorFlushed.fire();
|
||||
mouseUpListener.dispose();
|
||||
if (this.monitor) {
|
||||
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 readonly _onDidChange = new Emitter<number>();
|
||||
readonly onDidChange: Event<number> = this._onDidChange.event;
|
||||
|
||||
private readonly _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.addDisposableGenericMouseDownListner(this.domNode, 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);
|
||||
this.domNode.classList.add('grabbing');
|
||||
|
||||
if (e.target !== this.slider) {
|
||||
this.onDidChangeTop(e.offsetY);
|
||||
}
|
||||
|
||||
monitor.startMonitoring(<HTMLElement>e.target, e.buttons, standardMouseMoveMerger, event => this.onDidChangeTop(event.posy - origin.top), () => null);
|
||||
|
||||
const mouseUpListener = dom.addDisposableGenericMouseUpListner(document, () => {
|
||||
this._onColorFlushed.fire();
|
||||
mouseUpListener.dispose();
|
||||
monitor.stopMonitoring(true);
|
||||
this.domNode.classList.remove('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);
|
||||
this.domNode.classList.add('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);
|
||||
this.domNode.classList.add('hue-strip');
|
||||
}
|
||||
|
||||
protected getValue(color: Color): number {
|
||||
return 1 - (color.hsva.h / 360);
|
||||
}
|
||||
}
|
||||
|
||||
export class ColorPickerWidget extends Widget {
|
||||
|
||||
private static readonly ID = 'editor.contrib.colorPickerWidget';
|
||||
|
||||
body: ColorPickerBody;
|
||||
|
||||
constructor(container: Node, private readonly model: ColorPickerModel, private pixelRatio: number, themeService: IThemeService) {
|
||||
super();
|
||||
|
||||
this._register(onDidChangeZoomLevel(() => this.layout()));
|
||||
|
||||
const element = $('.colorpicker-widget');
|
||||
container.appendChild(element);
|
||||
|
||||
const header = new ColorPickerHeader(element, this.model, themeService);
|
||||
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 |
203
lib/vscode/src/vs/editor/contrib/comment/blockCommentCommand.ts
Normal file
203
lib/vscode/src/vs/editor/contrib/comment/blockCommentCommand.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
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 { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel, IIdentifiedSingleEditOperation } from 'vs/editor/common/model';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
|
||||
export class BlockCommentCommand implements ICommand {
|
||||
|
||||
private readonly _selection: Selection;
|
||||
private readonly _insertSpace: boolean;
|
||||
private _usedEndToken: string | null;
|
||||
|
||||
constructor(selection: Selection, insertSpace: boolean) {
|
||||
this._selection = selection;
|
||||
this._insertSpace = insertSpace;
|
||||
this._usedEndToken = null;
|
||||
}
|
||||
|
||||
public static _haystackHasNeedleAtOffset(haystack: string, needle: string, offset: number): boolean {
|
||||
if (offset < 0) {
|
||||
return false;
|
||||
}
|
||||
const needleLength = needle.length;
|
||||
const haystackLength = haystack.length;
|
||||
if (offset + needleLength > haystackLength) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < needleLength; i++) {
|
||||
const codeA = haystack.charCodeAt(offset + i);
|
||||
const codeB = needle.charCodeAt(i);
|
||||
|
||||
if (codeA === codeB) {
|
||||
continue;
|
||||
}
|
||||
if (codeA >= CharCode.A && codeA <= CharCode.Z && codeA + 32 === codeB) {
|
||||
// codeA is upper-case variant of codeB
|
||||
continue;
|
||||
}
|
||||
if (codeB >= CharCode.A && codeB <= CharCode.Z && codeB + 32 === codeA) {
|
||||
// codeB is upper-case variant of codeA
|
||||
continue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private _createOperationsForBlockComment(selection: Range, startToken: string, endToken: string, insertSpace: boolean, model: ITextModel, builder: IEditOperationBuilder): void {
|
||||
const startLineNumber = selection.startLineNumber;
|
||||
const startColumn = selection.startColumn;
|
||||
const endLineNumber = selection.endLineNumber;
|
||||
const endColumn = selection.endColumn;
|
||||
|
||||
const startLineText = model.getLineContent(startLineNumber);
|
||||
const endLineText = model.getLineContent(endLineNumber);
|
||||
|
||||
let startTokenIndex = startLineText.lastIndexOf(startToken, startColumn - 1 + startToken.length);
|
||||
let endTokenIndex = endLineText.indexOf(endToken, endColumn - 1 - endToken.length);
|
||||
|
||||
if (startTokenIndex !== -1 && endTokenIndex !== -1) {
|
||||
|
||||
if (startLineNumber === endLineNumber) {
|
||||
const lineBetweenTokens = startLineText.substring(startTokenIndex + startToken.length, endTokenIndex);
|
||||
|
||||
if (lineBetweenTokens.indexOf(endToken) >= 0) {
|
||||
// force to add a block comment
|
||||
startTokenIndex = -1;
|
||||
endTokenIndex = -1;
|
||||
}
|
||||
} else {
|
||||
const startLineAfterStartToken = startLineText.substring(startTokenIndex + startToken.length);
|
||||
const endLineBeforeEndToken = endLineText.substring(0, endTokenIndex);
|
||||
|
||||
if (startLineAfterStartToken.indexOf(endToken) >= 0 || endLineBeforeEndToken.indexOf(endToken) >= 0) {
|
||||
// force to add a block comment
|
||||
startTokenIndex = -1;
|
||||
endTokenIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ops: IIdentifiedSingleEditOperation[];
|
||||
|
||||
if (startTokenIndex !== -1 && endTokenIndex !== -1) {
|
||||
// Consider spaces as part of the comment tokens
|
||||
if (insertSpace && startTokenIndex + startToken.length < startLineText.length && startLineText.charCodeAt(startTokenIndex + startToken.length) === CharCode.Space) {
|
||||
// Pretend the start token contains a trailing space
|
||||
startToken = startToken + ' ';
|
||||
}
|
||||
|
||||
if (insertSpace && endTokenIndex > 0 && endLineText.charCodeAt(endTokenIndex - 1) === CharCode.Space) {
|
||||
// Pretend the end token contains a leading space
|
||||
endToken = ' ' + endToken;
|
||||
endTokenIndex -= 1;
|
||||
}
|
||||
ops = BlockCommentCommand._createRemoveBlockCommentOperations(
|
||||
new Range(startLineNumber, startTokenIndex + startToken.length + 1, endLineNumber, endTokenIndex + 1), startToken, endToken
|
||||
);
|
||||
} else {
|
||||
ops = BlockCommentCommand._createAddBlockCommentOperations(selection, startToken, endToken, this._insertSpace);
|
||||
this._usedEndToken = ops.length === 1 ? endToken : null;
|
||||
}
|
||||
|
||||
for (const op of ops) {
|
||||
builder.addTrackedEditOperation(op.range, op.text);
|
||||
}
|
||||
}
|
||||
|
||||
public static _createRemoveBlockCommentOperations(r: Range, startToken: string, endToken: string): IIdentifiedSingleEditOperation[] {
|
||||
let res: 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, insertSpace: boolean): IIdentifiedSingleEditOperation[] {
|
||||
let res: IIdentifiedSingleEditOperation[] = [];
|
||||
|
||||
if (!Range.isEmpty(r)) {
|
||||
// Insert block comment start
|
||||
res.push(EditOperation.insert(new Position(r.startLineNumber, r.startColumn), startToken + (insertSpace ? ' ' : '')));
|
||||
|
||||
// Insert block comment end
|
||||
res.push(EditOperation.insert(new Position(r.endLineNumber, r.endColumn), (insertSpace ? ' ' : '') + 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: ITextModel, builder: IEditOperationBuilder): void {
|
||||
const startLineNumber = this._selection.startLineNumber;
|
||||
const startColumn = this._selection.startColumn;
|
||||
|
||||
model.tokenizeIfCheap(startLineNumber);
|
||||
const languageId = model.getLanguageIdAtPosition(startLineNumber, startColumn);
|
||||
const config = LanguageConfigurationRegistry.getComments(languageId);
|
||||
if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) {
|
||||
// Mode does not support block comments
|
||||
return;
|
||||
}
|
||||
|
||||
this._createOperationsForBlockComment(this._selection, config.blockCommentStartToken, config.blockCommentEndToken, this._insertSpace, model, builder);
|
||||
}
|
||||
|
||||
public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
|
||||
const inverseEditOperations = helper.getInverseEditOperations();
|
||||
if (inverseEditOperations.length === 2) {
|
||||
const startTokenEditOperation = inverseEditOperations[0];
|
||||
const endTokenEditOperation = inverseEditOperations[1];
|
||||
|
||||
return new Selection(
|
||||
startTokenEditOperation.range.endLineNumber,
|
||||
startTokenEditOperation.range.endColumn,
|
||||
endTokenEditOperation.range.startLineNumber,
|
||||
endTokenEditOperation.range.startColumn
|
||||
);
|
||||
} else {
|
||||
const srcRange = inverseEditOperations[0].range;
|
||||
const 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
153
lib/vscode/src/vs/editor/contrib/comment/comment.ts
Normal file
153
lib/vscode/src/vs/editor/contrib/comment/comment.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, IActionOptions, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICommand } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { BlockCommentCommand } from 'vs/editor/contrib/comment/blockCommentCommand';
|
||||
import { LineCommentCommand, Type } from 'vs/editor/contrib/comment/lineCommentCommand';
|
||||
import { MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
|
||||
abstract class CommentLineAction extends EditorAction {
|
||||
|
||||
private readonly _type: Type;
|
||||
|
||||
constructor(type: Type, opts: IActionOptions) {
|
||||
super(opts);
|
||||
this._type = type;
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = editor.getModel();
|
||||
const commands: ICommand[] = [];
|
||||
const selections = editor.getSelections();
|
||||
const modelOptions = model.getOptions();
|
||||
const commentsOptions = editor.getOption(EditorOption.comments);
|
||||
|
||||
for (const selection of selections) {
|
||||
commands.push(new LineCommentCommand(
|
||||
selection,
|
||||
modelOptions.tabSize,
|
||||
this._type,
|
||||
commentsOptions.insertSpace,
|
||||
commentsOptions.ignoreEmptyLines
|
||||
));
|
||||
}
|
||||
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, commands);
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.US_SLASH,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarEditMenu,
|
||||
group: '5_insert',
|
||||
title: nls.localize({ key: 'miToggleLineComment', comment: ['&& denotes a mnemonic'] }, "&&Toggle Line Comment"),
|
||||
order: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_C),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_U),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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.editorTextFocus,
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_A,
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_A },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarEditMenu,
|
||||
group: '5_insert',
|
||||
title: nls.localize({ key: 'miToggleBlockComment', comment: ['&& denotes a mnemonic'] }, "Toggle &&Block Comment"),
|
||||
order: 2
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const commentsOptions = editor.getOption(EditorOption.comments);
|
||||
const commands: ICommand[] = [];
|
||||
const selections = editor.getSelections();
|
||||
for (const selection of selections) {
|
||||
commands.push(new BlockCommentCommand(selection, commentsOptions.insertSpace));
|
||||
}
|
||||
|
||||
editor.pushUndoStop();
|
||||
editor.executeCommands(this.id, commands);
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(ToggleCommentLineAction);
|
||||
registerEditorAction(AddLineCommentAction);
|
||||
registerEditorAction(RemoveLineCommentAction);
|
||||
registerEditorAction(BlockCommentAction);
|
||||
458
lib/vscode/src/vs/editor/contrib/comment/lineCommentCommand.ts
Normal file
458
lib/vscode/src/vs/editor/contrib/comment/lineCommentCommand.ts
Normal file
@@ -0,0 +1,458 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
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 { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon';
|
||||
import { IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { BlockCommentCommand } from 'vs/editor/contrib/comment/blockCommentCommand';
|
||||
import { Constants } from 'vs/base/common/uint';
|
||||
|
||||
export interface IInsertionPoint {
|
||||
ignore: boolean;
|
||||
commentStrOffset: number;
|
||||
}
|
||||
|
||||
export interface ILinePreflightData {
|
||||
ignore: boolean;
|
||||
commentStr: string;
|
||||
commentStrOffset: number;
|
||||
commentStrLength: number;
|
||||
}
|
||||
|
||||
export interface IPreflightDataSupported {
|
||||
supported: true;
|
||||
shouldRemoveComments: boolean;
|
||||
lines: ILinePreflightData[];
|
||||
}
|
||||
export interface IPreflightDataUnsupported {
|
||||
supported: false;
|
||||
}
|
||||
export type IPreflightData = IPreflightDataSupported | IPreflightDataUnsupported;
|
||||
|
||||
export interface ISimpleModel {
|
||||
getLineContent(lineNumber: number): string;
|
||||
}
|
||||
|
||||
export const enum Type {
|
||||
Toggle = 0,
|
||||
ForceAdd = 1,
|
||||
ForceRemove = 2
|
||||
}
|
||||
|
||||
export class LineCommentCommand implements ICommand {
|
||||
|
||||
private readonly _selection: Selection;
|
||||
private readonly _tabSize: number;
|
||||
private readonly _type: Type;
|
||||
private readonly _insertSpace: boolean;
|
||||
private readonly _ignoreEmptyLines: boolean;
|
||||
private _selectionId: string | null;
|
||||
private _deltaColumn: number;
|
||||
private _moveEndPositionDown: boolean;
|
||||
|
||||
constructor(
|
||||
selection: Selection,
|
||||
tabSize: number,
|
||||
type: Type,
|
||||
insertSpace: boolean,
|
||||
ignoreEmptyLines: boolean
|
||||
) {
|
||||
this._selection = selection;
|
||||
this._tabSize = tabSize;
|
||||
this._type = type;
|
||||
this._insertSpace = insertSpace;
|
||||
this._selectionId = null;
|
||||
this._deltaColumn = 0;
|
||||
this._moveEndPositionDown = false;
|
||||
this._ignoreEmptyLines = ignoreEmptyLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: ITextModel, startLineNumber: number, endLineNumber: number): ILinePreflightData[] | null {
|
||||
|
||||
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, insertSpace: boolean, model: ISimpleModel, lines: ILinePreflightData[], startLineNumber: number, ignoreEmptyLines: boolean): IPreflightData {
|
||||
let onlyWhitespaceLines = true;
|
||||
|
||||
let shouldRemoveComments: boolean;
|
||||
if (type === Type.Toggle) {
|
||||
shouldRemoveComments = true;
|
||||
} else if (type === Type.ForceAdd) {
|
||||
shouldRemoveComments = false;
|
||||
} else {
|
||||
shouldRemoveComments = true;
|
||||
}
|
||||
|
||||
for (let i = 0, lineCount = lines.length; i < lineCount; i++) {
|
||||
const lineData = lines[i];
|
||||
const lineNumber = startLineNumber + i;
|
||||
|
||||
const lineContent = model.getLineContent(lineNumber);
|
||||
const lineContentStartOffset = strings.firstNonWhitespaceIndex(lineContent);
|
||||
|
||||
if (lineContentStartOffset === -1) {
|
||||
// Empty or whitespace only line
|
||||
lineData.ignore = ignoreEmptyLines;
|
||||
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 && insertSpace) {
|
||||
// Remove a following space if present
|
||||
const 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 (let 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, insertSpace: boolean, model: ITextModel, startLineNumber: number, endLineNumber: number, ignoreEmptyLines: boolean): IPreflightData {
|
||||
const lines = LineCommentCommand._gatherPreflightCommentStrings(model, startLineNumber, endLineNumber);
|
||||
if (lines === null) {
|
||||
return {
|
||||
supported: false
|
||||
};
|
||||
}
|
||||
|
||||
return LineCommentCommand._analyzeLines(type, insertSpace, model, lines, startLineNumber, ignoreEmptyLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a successful analysis, execute either insert line comments, either remove line comments
|
||||
*/
|
||||
private _executeLineComments(model: ISimpleModel, builder: IEditOperationBuilder, data: IPreflightDataSupported, s: Selection): void {
|
||||
|
||||
let ops: IIdentifiedSingleEditOperation[];
|
||||
|
||||
if (data.shouldRemoveComments) {
|
||||
ops = LineCommentCommand._createRemoveLineCommentsOperations(data.lines, s.startLineNumber);
|
||||
} else {
|
||||
LineCommentCommand._normalizeInsertionPoint(model, data.lines, s.startLineNumber, this._tabSize);
|
||||
ops = this._createAddLineCommentsOperations(data.lines, s.startLineNumber);
|
||||
}
|
||||
|
||||
const cursorPosition = new Position(s.positionLineNumber, s.positionColumn);
|
||||
|
||||
for (let i = 0, len = ops.length; i < len; i++) {
|
||||
builder.addEditOperation(ops[i].range, ops[i].text);
|
||||
if (Range.isEmpty(ops[i].range) && Range.getStartPosition(ops[i].range).equals(cursorPosition)) {
|
||||
const lineContent = model.getLineContent(cursorPosition.lineNumber);
|
||||
if (lineContent.length + 1 === cursorPosition.column) {
|
||||
this._deltaColumn = (ops[i].text || '').length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._selectionId = builder.trackSelection(s);
|
||||
}
|
||||
|
||||
private _attemptRemoveBlockComment(model: ITextModel, s: Selection, startToken: string, endToken: string): IIdentifiedSingleEditOperation[] | null {
|
||||
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: ITextModel, builder: IEditOperationBuilder, s: Selection): void {
|
||||
model.tokenizeIfCheap(s.startLineNumber);
|
||||
let languageId = model.getLanguageIdAtPosition(s.startLineNumber, 1);
|
||||
let config = LanguageConfigurationRegistry.getComments(languageId);
|
||||
if (!config || !config.blockCommentStartToken || !config.blockCommentEndToken) {
|
||||
// Mode does not support block comments
|
||||
return;
|
||||
}
|
||||
|
||||
const startToken = config.blockCommentStartToken;
|
||||
const endToken = config.blockCommentEndToken;
|
||||
|
||||
let ops = this._attemptRemoveBlockComment(model, s, startToken, endToken);
|
||||
if (!ops) {
|
||||
if (s.isEmpty()) {
|
||||
const lineContent = model.getLineContent(s.startLineNumber);
|
||||
let 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,
|
||||
this._insertSpace
|
||||
);
|
||||
} else {
|
||||
ops = BlockCommentCommand._createAddBlockCommentOperations(
|
||||
new Range(s.startLineNumber, model.getLineFirstNonWhitespaceColumn(s.startLineNumber), s.endLineNumber, model.getLineMaxColumn(s.endLineNumber)),
|
||||
startToken,
|
||||
endToken,
|
||||
this._insertSpace
|
||||
);
|
||||
}
|
||||
|
||||
if (ops.length === 1) {
|
||||
// Leave cursor after token and Space
|
||||
this._deltaColumn = startToken.length + 1;
|
||||
}
|
||||
}
|
||||
this._selectionId = builder.trackSelection(s);
|
||||
for (const op of ops) {
|
||||
builder.addEditOperation(op.range, op.text);
|
||||
}
|
||||
}
|
||||
|
||||
public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
|
||||
|
||||
let 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));
|
||||
}
|
||||
|
||||
const data = LineCommentCommand._gatherPreflightData(
|
||||
this._type,
|
||||
this._insertSpace,
|
||||
model,
|
||||
s.startLineNumber,
|
||||
s.endLineNumber,
|
||||
this._ignoreEmptyLines
|
||||
);
|
||||
|
||||
if (data.supported) {
|
||||
return this._executeLineComments(model, builder, data, s);
|
||||
}
|
||||
|
||||
return this._executeBlockComment(model, builder, s);
|
||||
}
|
||||
|
||||
public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
|
||||
let result = helper.getTrackedSelection(this._selectionId!);
|
||||
|
||||
if (this._moveEndPositionDown) {
|
||||
result = result.setEndPosition(result.endLineNumber + 1, 1);
|
||||
}
|
||||
|
||||
return new Selection(
|
||||
result.selectionStartLineNumber,
|
||||
result.selectionStartColumn + this._deltaColumn,
|
||||
result.positionLineNumber,
|
||||
result.positionColumn + this._deltaColumn
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate edit operations in the remove line comment case
|
||||
*/
|
||||
public static _createRemoveLineCommentsOperations(lines: ILinePreflightData[], startLineNumber: number): IIdentifiedSingleEditOperation[] {
|
||||
let res: IIdentifiedSingleEditOperation[] = [];
|
||||
|
||||
for (let i = 0, len = lines.length; i < len; i++) {
|
||||
const 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
|
||||
*/
|
||||
private _createAddLineCommentsOperations(lines: ILinePreflightData[], startLineNumber: number): IIdentifiedSingleEditOperation[] {
|
||||
let res: IIdentifiedSingleEditOperation[] = [];
|
||||
const afterCommentStr = this._insertSpace ? ' ' : '';
|
||||
|
||||
|
||||
for (let i = 0, len = lines.length; i < len; i++) {
|
||||
const lineData = lines[i];
|
||||
|
||||
if (lineData.ignore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
res.push(EditOperation.insert(new Position(startLineNumber + i, lineData.commentStrOffset + 1), lineData.commentStr + afterCommentStr));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
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 {
|
||||
let minVisibleColumn = Constants.MAX_SAFE_SMALL_INTEGER;
|
||||
let j: number;
|
||||
let lenJ: number;
|
||||
|
||||
for (let i = 0, len = lines.length; i < len; i++) {
|
||||
if (lines[i].ignore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineContent = model.getLineContent(startLineNumber + i);
|
||||
|
||||
let currentVisibleColumn = 0;
|
||||
for (let 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 (let i = 0, len = lines.length; i < len; i++) {
|
||||
if (lines[i].ignore) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineContent = model.getLineContent(startLineNumber + i);
|
||||
|
||||
let currentVisibleColumn = 0;
|
||||
for (j = 0, lenJ = lines[i].commentStrOffset; currentVisibleColumn < minVisibleColumn && j < lenJ; j++) {
|
||||
currentVisibleColumn = LineCommentCommand.nextVisibleColumn(currentVisibleColumn, tabSize, lineContent.charCodeAt(j) === CharCode.Tab, 1);
|
||||
}
|
||||
|
||||
if (currentVisibleColumn > minVisibleColumn) {
|
||||
lines[i].commentStrOffset = j - 1;
|
||||
} else {
|
||||
lines[i].commentStrOffset = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,512 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { BlockCommentCommand } from 'vs/editor/contrib/comment/blockCommentCommand';
|
||||
import { testCommand } from 'vs/editor/test/browser/testCommand';
|
||||
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, true), 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', () => {
|
||||
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)
|
||||
);
|
||||
});
|
||||
|
||||
test('issue #34618', function () {
|
||||
testBlockCommentCommand(
|
||||
[
|
||||
'<0 0> middle end',
|
||||
],
|
||||
new Selection(1, 4, 1, 4),
|
||||
[
|
||||
' middle end'
|
||||
],
|
||||
new Selection(1, 1, 1, 1)
|
||||
);
|
||||
});
|
||||
|
||||
test('', () => {
|
||||
});
|
||||
|
||||
test('insertSpace false', () => {
|
||||
function testLineCommentCommand(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, false), expectedLines, expectedSelection);
|
||||
mode.dispose();
|
||||
}
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'some text'
|
||||
],
|
||||
new Selection(1, 1, 1, 5),
|
||||
[
|
||||
'<0some0> text'
|
||||
],
|
||||
new Selection(1, 3, 1, 7)
|
||||
);
|
||||
});
|
||||
|
||||
test('insertSpace false does not remove space', () => {
|
||||
function testLineCommentCommand(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, false), expectedLines, expectedSelection);
|
||||
mode.dispose();
|
||||
}
|
||||
|
||||
testLineCommentCommand(
|
||||
[
|
||||
'<0 some 0> text'
|
||||
],
|
||||
new Selection(1, 4, 1, 8),
|
||||
[
|
||||
' some text'
|
||||
],
|
||||
new Selection(1, 1, 1, 7)
|
||||
);
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
284
lib/vscode/src/vs/editor/contrib/contextmenu/contextmenu.ts
Normal file
284
lib/vscode/src/vs/editor/contrib/contextmenu/contextmenu.ts
Normal file
@@ -0,0 +1,284 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IAnchor } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { IAction, Separator, SubmenuAction } from 'vs/base/common/actions';
|
||||
import { KeyCode, KeyMod, ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { IMenuService, MenuId, SubmenuItemAction } from 'vs/platform/actions/common/actions';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
|
||||
export class ContextMenuController implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.contextmenu';
|
||||
|
||||
public static get(editor: ICodeEditor): ContextMenuController {
|
||||
return editor.getContribution<ContextMenuController>(ContextMenuController.ID);
|
||||
}
|
||||
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private _contextMenuIsBeingShownCount: number = 0;
|
||||
private readonly _editor: ICodeEditor;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
|
||||
@IContextViewService private readonly _contextViewService: IContextViewService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IMenuService private readonly _menuService: IMenuService
|
||||
) {
|
||||
this._editor = editor;
|
||||
|
||||
this._toDispose.add(this._editor.onContextMenu((e: IEditorMouseEvent) => this._onContextMenu(e)));
|
||||
this._toDispose.add(this._editor.onMouseWheel((e: IMouseWheelEvent) => {
|
||||
if (this._contextMenuIsBeingShownCount > 0) {
|
||||
const view = this._contextViewService.getContextViewElement();
|
||||
const target = e.srcElement as HTMLElement;
|
||||
|
||||
// Event triggers on shadow root host first
|
||||
// Check if the context view is under this host before hiding it #103169
|
||||
if (!(target.shadowRoot && dom.getShadowRoot(view) === target.shadowRoot)) {
|
||||
this._contextViewService.hideContextView();
|
||||
}
|
||||
}
|
||||
}));
|
||||
this._toDispose.add(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.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._editor.getOption(EditorOption.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) {
|
||||
let hasSelectionAtPosition = false;
|
||||
for (const selection of this._editor.getSelections()) {
|
||||
if (selection.containsPosition(e.target.position)) {
|
||||
hasSelectionAtPosition = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSelectionAtPosition) {
|
||||
this._editor.setPosition(e.target.position);
|
||||
}
|
||||
}
|
||||
|
||||
// Unless the user triggerd the context menu through Shift+F10, use the mouse position as menu position
|
||||
let anchor: IAnchor | null = null;
|
||||
if (e.target.type !== MouseTargetType.TEXTAREA) {
|
||||
anchor = { x: e.event.posx - 1, width: 2, y: e.event.posy - 1, height: 2 };
|
||||
}
|
||||
|
||||
// Show the context menu
|
||||
this.showContextMenu(anchor);
|
||||
}
|
||||
|
||||
public showContextMenu(anchor?: IAnchor | null): void {
|
||||
if (!this._editor.getOption(EditorOption.contextmenu)) {
|
||||
return; // Context menu is turned off through configuration
|
||||
}
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._contextMenuService) {
|
||||
this._editor.focus();
|
||||
return; // We need the context menu service to function
|
||||
}
|
||||
|
||||
// Find actions available for menu
|
||||
const menuActions = this._getMenuActions(this._editor.getModel(), MenuId.EditorContext);
|
||||
|
||||
// Show menu if we have actions to show
|
||||
if (menuActions.length > 0) {
|
||||
this._doShowContextMenu(menuActions, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
private _getMenuActions(model: ITextModel, menuId: MenuId): IAction[] {
|
||||
const result: IAction[] = [];
|
||||
|
||||
// get menu groups
|
||||
const menu = this._menuService.createMenu(menuId, this._contextKeyService);
|
||||
const groups = menu.getActions({ arg: model.uri });
|
||||
menu.dispose();
|
||||
|
||||
// translate them into other actions
|
||||
for (let group of groups) {
|
||||
const [, actions] = group;
|
||||
let addedItems = 0;
|
||||
for (const action of actions) {
|
||||
if (action instanceof SubmenuItemAction) {
|
||||
const subActions = this._getMenuActions(model, action.item.submenu);
|
||||
if (subActions.length > 0) {
|
||||
result.push(new SubmenuAction(action.id, action.label, subActions));
|
||||
addedItems++;
|
||||
}
|
||||
} else {
|
||||
result.push(action);
|
||||
addedItems++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedItems) {
|
||||
result.push(new Separator());
|
||||
}
|
||||
}
|
||||
|
||||
if (result.length) {
|
||||
result.pop(); // remove last separator
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private _doShowContextMenu(actions: IAction[], anchor: IAnchor | null = null): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable hover
|
||||
const oldHoverSetting = this._editor.getOption(EditorOption.hover);
|
||||
this._editor.updateOptions({
|
||||
hover: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
|
||||
if (!anchor) {
|
||||
// Ensure selection is visible
|
||||
this._editor.revealPosition(this._editor.getPosition(), ScrollType.Immediate);
|
||||
|
||||
this._editor.render();
|
||||
const cursorCoords = this._editor.getScrolledVisiblePosition(this._editor.getPosition());
|
||||
|
||||
// Translate to absolute editor position
|
||||
const editorCoords = dom.getDomNodePagePosition(this._editor.getDomNode());
|
||||
const posx = editorCoords.left + cursorCoords.left;
|
||||
const posy = editorCoords.top + cursorCoords.top + cursorCoords.height;
|
||||
|
||||
anchor = { x: posx, y: posy };
|
||||
}
|
||||
|
||||
// Show menu
|
||||
this._contextMenuIsBeingShownCount++;
|
||||
this._contextMenuService.showContextMenu({
|
||||
domForShadowRoot: this._editor.getDomNode(),
|
||||
|
||||
getAnchor: () => anchor!,
|
||||
|
||||
getActions: () => actions,
|
||||
|
||||
getActionViewItem: (action) => {
|
||||
const keybinding = this._keybindingFor(action);
|
||||
if (keybinding) {
|
||||
return new ActionViewItem(action, action, { label: true, keybinding: keybinding.getLabel(), isMenu: true });
|
||||
}
|
||||
|
||||
const customActionViewItem = <any>action;
|
||||
if (typeof customActionViewItem.getActionViewItem === 'function') {
|
||||
return customActionViewItem.getActionViewItem();
|
||||
}
|
||||
|
||||
return new ActionViewItem(action, action, { icon: true, label: true, isMenu: true });
|
||||
},
|
||||
|
||||
getKeyBinding: (action): ResolvedKeybinding | undefined => {
|
||||
return this._keybindingFor(action);
|
||||
},
|
||||
|
||||
onHide: (wasCancelled: boolean) => {
|
||||
this._contextMenuIsBeingShownCount--;
|
||||
this._editor.focus();
|
||||
this._editor.updateOptions({
|
||||
hover: oldHoverSetting
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _keybindingFor(action: IAction): ResolvedKeybinding | undefined {
|
||||
return this._keybindingService.lookupKeybinding(action.id);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._contextMenuIsBeingShownCount > 0) {
|
||||
this._contextViewService.hideContextView();
|
||||
}
|
||||
|
||||
this._toDispose.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
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: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.Shift | KeyCode.F10,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
let contribution = ContextMenuController.get(editor);
|
||||
contribution.showContextMenu();
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(ContextMenuController.ID, ContextMenuController);
|
||||
registerEditorAction(ShowContextMenu);
|
||||
165
lib/vscode/src/vs/editor/contrib/cursorUndo/cursorUndo.ts
Normal file
165
lib/vscode/src/vs/editor/contrib/cursorUndo/cursorUndo.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
|
||||
class CursorState {
|
||||
readonly selections: readonly Selection[];
|
||||
|
||||
constructor(selections: readonly 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;
|
||||
}
|
||||
}
|
||||
|
||||
class StackElement {
|
||||
constructor(
|
||||
public readonly cursorState: CursorState,
|
||||
public readonly scrollTop: number,
|
||||
public readonly scrollLeft: number
|
||||
) { }
|
||||
}
|
||||
|
||||
export class CursorUndoRedoController extends Disposable implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.cursorUndoRedoController';
|
||||
|
||||
public static get(editor: ICodeEditor): CursorUndoRedoController {
|
||||
return editor.getContribution<CursorUndoRedoController>(CursorUndoRedoController.ID);
|
||||
}
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
private _isCursorUndoRedo: boolean;
|
||||
|
||||
private _undoStack: StackElement[];
|
||||
private _redoStack: StackElement[];
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
super();
|
||||
this._editor = editor;
|
||||
this._isCursorUndoRedo = false;
|
||||
|
||||
this._undoStack = [];
|
||||
this._redoStack = [];
|
||||
|
||||
this._register(editor.onDidChangeModel((e) => {
|
||||
this._undoStack = [];
|
||||
this._redoStack = [];
|
||||
}));
|
||||
this._register(editor.onDidChangeModelContent((e) => {
|
||||
this._undoStack = [];
|
||||
this._redoStack = [];
|
||||
}));
|
||||
this._register(editor.onDidChangeCursorSelection((e) => {
|
||||
if (this._isCursorUndoRedo) {
|
||||
return;
|
||||
}
|
||||
if (!e.oldSelections) {
|
||||
return;
|
||||
}
|
||||
if (e.oldModelVersionId !== e.modelVersionId) {
|
||||
return;
|
||||
}
|
||||
const prevState = new CursorState(e.oldSelections);
|
||||
const isEqualToLastUndoStack = (this._undoStack.length > 0 && this._undoStack[this._undoStack.length - 1].cursorState.equals(prevState));
|
||||
if (!isEqualToLastUndoStack) {
|
||||
this._undoStack.push(new StackElement(prevState, editor.getScrollTop(), editor.getScrollLeft()));
|
||||
this._redoStack = [];
|
||||
if (this._undoStack.length > 50) {
|
||||
// keep the cursor undo stack bounded
|
||||
this._undoStack.shift();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public cursorUndo(): void {
|
||||
if (!this._editor.hasModel() || this._undoStack.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._redoStack.push(new StackElement(new CursorState(this._editor.getSelections()), this._editor.getScrollTop(), this._editor.getScrollLeft()));
|
||||
this._applyState(this._undoStack.pop()!);
|
||||
}
|
||||
|
||||
public cursorRedo(): void {
|
||||
if (!this._editor.hasModel() || this._redoStack.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._undoStack.push(new StackElement(new CursorState(this._editor.getSelections()), this._editor.getScrollTop(), this._editor.getScrollLeft()));
|
||||
this._applyState(this._redoStack.pop()!);
|
||||
}
|
||||
|
||||
private _applyState(stackElement: StackElement): void {
|
||||
this._isCursorUndoRedo = true;
|
||||
this._editor.setSelections(stackElement.cursorState.selections);
|
||||
this._editor.setScrollPosition({
|
||||
scrollTop: stackElement.scrollTop,
|
||||
scrollLeft: stackElement.scrollLeft
|
||||
});
|
||||
this._isCursorUndoRedo = false;
|
||||
}
|
||||
}
|
||||
|
||||
export class CursorUndo extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'cursorUndo',
|
||||
label: nls.localize('cursor.undo', "Cursor Undo"),
|
||||
alias: 'Cursor Undo',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_U,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {
|
||||
CursorUndoRedoController.get(editor).cursorUndo();
|
||||
}
|
||||
}
|
||||
|
||||
export class CursorRedo extends EditorAction {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'cursorRedo',
|
||||
label: nls.localize('cursor.redo', "Cursor Redo"),
|
||||
alias: 'Cursor Redo',
|
||||
precondition: undefined
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {
|
||||
CursorUndoRedoController.get(editor).cursorRedo();
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(CursorUndoRedoController.ID, CursorUndoRedoController);
|
||||
registerEditorAction(CursorUndo);
|
||||
registerEditorAction(CursorRedo);
|
||||
@@ -0,0 +1,63 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
import { CursorUndo, CursorUndoRedoController } from 'vs/editor/contrib/cursorUndo/cursorUndo';
|
||||
import { Handler } from 'vs/editor/common/editorCommon';
|
||||
import { CoreNavigationCommands, CoreEditingCommands } from 'vs/editor/browser/controller/coreCommands';
|
||||
|
||||
suite('FindController', () => {
|
||||
|
||||
const cursorUndoAction = new CursorUndo();
|
||||
|
||||
test('issue #82535: Edge case with cursorUndo', () => {
|
||||
withTestCodeEditor([
|
||||
''
|
||||
], {}, (editor) => {
|
||||
|
||||
editor.registerAndInstantiateContribution(CursorUndoRedoController.ID, CursorUndoRedoController);
|
||||
|
||||
// type hello
|
||||
editor.trigger('test', Handler.Type, { text: 'hello' });
|
||||
|
||||
// press left
|
||||
CoreNavigationCommands.CursorLeft.runEditorCommand(null, editor, {});
|
||||
|
||||
// press Delete
|
||||
CoreEditingCommands.DeleteRight.runEditorCommand(null, editor, {});
|
||||
assert.deepEqual(editor.getValue(), 'hell');
|
||||
assert.deepEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]);
|
||||
|
||||
// press left
|
||||
CoreNavigationCommands.CursorLeft.runEditorCommand(null, editor, {});
|
||||
assert.deepEqual(editor.getSelections(), [new Selection(1, 4, 1, 4)]);
|
||||
|
||||
// press Ctrl+U
|
||||
cursorUndoAction.run(null!, editor, {});
|
||||
assert.deepEqual(editor.getSelections(), [new Selection(1, 5, 1, 5)]);
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #82535: Edge case with cursorUndo (reverse)', () => {
|
||||
withTestCodeEditor([
|
||||
''
|
||||
], {}, (editor) => {
|
||||
|
||||
editor.registerAndInstantiateContribution(CursorUndoRedoController.ID, CursorUndoRedoController);
|
||||
|
||||
// type hello
|
||||
editor.trigger('test', Handler.Type, { text: 'hell' });
|
||||
editor.trigger('test', Handler.Type, { text: 'o' });
|
||||
assert.deepEqual(editor.getValue(), 'hello');
|
||||
assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]);
|
||||
|
||||
// press Ctrl+U
|
||||
cursorUndoAction.run(null!, editor, {});
|
||||
assert.deepEqual(editor.getSelections(), [new Selection(1, 6, 1, 6)]);
|
||||
});
|
||||
});
|
||||
});
|
||||
28
lib/vscode/src/vs/editor/contrib/dnd/dnd.css
Normal file
28
lib/vscode/src/vs/editor/contrib/dnd/dnd.css
Normal file
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. 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;
|
||||
}
|
||||
232
lib/vscode/src/vs/editor/contrib/dnd/dnd.ts
Normal file
232
lib/vscode/src/vs/editor/contrib/dnd/dnd.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dnd';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ICodeEditor, IEditorMouseEvent, IMouseTarget, MouseTargetType, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { IEditorContribution, ScrollType } 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 'vs/editor/contrib/dnd/dragAndDropCommand';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { IModelDeltaDecoration } from 'vs/editor/common/model';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
function hasTriggerModifier(e: IKeyboardEvent | IMouseEvent): boolean {
|
||||
if (isMacintosh) {
|
||||
return e.altKey;
|
||||
} else {
|
||||
return e.ctrlKey;
|
||||
}
|
||||
}
|
||||
|
||||
export class DragAndDropController extends Disposable implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.dragAndDrop';
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
private _dragSelection: Selection | null;
|
||||
private _dndDecorationIds: string[];
|
||||
private _mouseDown: boolean;
|
||||
private _modifierPressed: boolean;
|
||||
static readonly TRIGGER_KEY_VALUE = isMacintosh ? KeyCode.Alt : KeyCode.Ctrl;
|
||||
|
||||
static get(editor: ICodeEditor): DragAndDropController {
|
||||
return editor.getContribution<DragAndDropController>(DragAndDropController.ID);
|
||||
}
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
super();
|
||||
this._editor = editor;
|
||||
this._register(this._editor.onMouseDown((e: IEditorMouseEvent) => this._onEditorMouseDown(e)));
|
||||
this._register(this._editor.onMouseUp((e: IEditorMouseEvent) => this._onEditorMouseUp(e)));
|
||||
this._register(this._editor.onMouseDrag((e: IEditorMouseEvent) => this._onEditorMouseDrag(e)));
|
||||
this._register(this._editor.onMouseDrop((e: IPartialEditorMouseEvent) => this._onEditorMouseDrop(e)));
|
||||
this._register(this._editor.onKeyDown((e: IKeyboardEvent) => this.onEditorKeyDown(e)));
|
||||
this._register(this._editor.onKeyUp((e: IKeyboardEvent) => this.onEditorKeyUp(e)));
|
||||
this._register(this._editor.onDidBlurEditorWidget(() => this.onEditorBlur()));
|
||||
this._register(this._editor.onDidBlurEditorText(() => this.onEditorBlur()));
|
||||
this._dndDecorationIds = [];
|
||||
this._mouseDown = false;
|
||||
this._modifierPressed = false;
|
||||
this._dragSelection = null;
|
||||
}
|
||||
|
||||
private onEditorBlur() {
|
||||
this._removeDecoration();
|
||||
this._dragSelection = null;
|
||||
this._mouseDown = false;
|
||||
this._modifierPressed = false;
|
||||
}
|
||||
|
||||
private onEditorKeyDown(e: IKeyboardEvent): void {
|
||||
if (!this._editor.getOption(EditorOption.dragAndDrop) || this._editor.getOption(EditorOption.columnSelection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasTriggerModifier(e)) {
|
||||
this._modifierPressed = true;
|
||||
}
|
||||
|
||||
if (this._mouseDown && hasTriggerModifier(e)) {
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'copy'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onEditorKeyUp(e: IKeyboardEvent): void {
|
||||
if (!this._editor.getOption(EditorOption.dragAndDrop) || this._editor.getOption(EditorOption.columnSelection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasTriggerModifier(e)) {
|
||||
this._modifierPressed = 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) {
|
||||
const selections = this._editor.getSelections() || [];
|
||||
let possibleSelections = selections.filter(selection => target.position && selection.containsPosition(target.position));
|
||||
if (possibleSelections.length === 1) {
|
||||
this._dragSelection = possibleSelections[0];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTriggerModifier(mouseEvent.event)) {
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'copy'
|
||||
});
|
||||
} else {
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'default'
|
||||
});
|
||||
}
|
||||
|
||||
if (target.position) {
|
||||
if (this._dragSelection.containsPosition(target.position)) {
|
||||
this._removeDecoration();
|
||||
} else {
|
||||
this.showAt(target.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onEditorMouseDrop(mouseEvent: IPartialEditorMouseEvent): 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: Selection[] | null = null;
|
||||
if (mouseEvent.event.shiftKey) {
|
||||
let primarySelection = this._editor.getSelection();
|
||||
if (primarySelection) {
|
||||
const { selectionStartLineNumber, selectionStartColumn } = primarySelection;
|
||||
newSelections = [new Selection(selectionStartLineNumber, selectionStartColumn, newCursorPosition.lineNumber, newCursorPosition.column)];
|
||||
}
|
||||
} else {
|
||||
newSelections = (this._editor.getSelections() || []).map(selection => {
|
||||
if (selection.containsPosition(newCursorPosition)) {
|
||||
return new Selection(newCursorPosition.lineNumber, newCursorPosition.column, newCursorPosition.lineNumber, newCursorPosition.column);
|
||||
} else {
|
||||
return selection;
|
||||
}
|
||||
});
|
||||
}
|
||||
// Use `mouse` as the source instead of `api`.
|
||||
(<CodeEditorWidget>this._editor).setSelections(newSelections || [], 'mouse');
|
||||
} else if (!this._dragSelection.containsPosition(newCursorPosition) ||
|
||||
(
|
||||
(
|
||||
hasTriggerModifier(mouseEvent.event) ||
|
||||
this._modifierPressed
|
||||
) && (
|
||||
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, hasTriggerModifier(mouseEvent.event) || this._modifierPressed));
|
||||
this._editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
|
||||
this._editor.updateOptions({
|
||||
mouseStyle: 'text'
|
||||
});
|
||||
|
||||
this._removeDecoration();
|
||||
this._dragSelection = null;
|
||||
this._mouseDown = false;
|
||||
}
|
||||
|
||||
private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({
|
||||
className: 'dnd-target'
|
||||
});
|
||||
|
||||
public showAt(position: Position): void {
|
||||
let newDecorations: IModelDeltaDecoration[] = [{
|
||||
range: new Range(position.lineNumber, position.column, position.lineNumber, position.column),
|
||||
options: DragAndDropController._DECORATION_OPTIONS
|
||||
}];
|
||||
|
||||
this._dndDecorationIds = this._editor.deltaDecorations(this._dndDecorationIds, newDecorations);
|
||||
this._editor.revealPosition(position, ScrollType.Immediate);
|
||||
}
|
||||
|
||||
private _removeDecoration(): void {
|
||||
this._dndDecorationIds = this._editor.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 dispose(): void {
|
||||
this._removeDecoration();
|
||||
this._dragSelection = null;
|
||||
this._mouseDown = false;
|
||||
this._modifierPressed = false;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(DragAndDropController.ID, DragAndDropController);
|
||||
108
lib/vscode/src/vs/editor/contrib/dnd/dragAndDropCommand.ts
Normal file
108
lib/vscode/src/vs/editor/contrib/dnd/dragAndDropCommand.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICommand, IEditOperationBuilder, ICursorStateComputerData } 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';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
|
||||
|
||||
export class DragAndDropCommand implements ICommand {
|
||||
|
||||
private readonly selection: Selection;
|
||||
private readonly targetPosition: Position;
|
||||
private targetSelection: Selection | null;
|
||||
private readonly copy: boolean;
|
||||
|
||||
constructor(selection: Selection, targetPosition: Position, copy: boolean) {
|
||||
this.selection = selection;
|
||||
this.targetPosition = targetPosition;
|
||||
this.copy = copy;
|
||||
this.targetSelection = null;
|
||||
}
|
||||
|
||||
public getEditOperations(model: ITextModel, builder: 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 position. 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: ITextModel, helper: ICursorStateComputerData): Selection {
|
||||
return this.targetSelection!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-list .monaco-list-row.focused.selected .outline-element .monaco-highlighted-label,
|
||||
.monaco-list .monaco-list-row.focused.selected .outline-element-decoration {
|
||||
/* make sure selection color wins when a label is being selected */
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.monaco-list .outline-element {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-list .outline-element .monaco-highlighted-label {
|
||||
color: var(--outline-element-color);
|
||||
}
|
||||
|
||||
.monaco-list .outline-element .monaco-icon-label-container .monaco-highlighted-label,
|
||||
.monaco-list .outline-element .monaco-icon-label-container .label-description {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-list .outline-element .outline-element-decoration {
|
||||
opacity: 0.75;
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
padding: 0 12px 0 5px;
|
||||
margin-left: auto;
|
||||
text-align: center;
|
||||
color: var(--outline-element-color);
|
||||
}
|
||||
|
||||
.monaco-list .outline-element .outline-element-decoration.bubble {
|
||||
font-family: codicon;
|
||||
font-size: 14px;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.monaco-list .outline-element .outline-element-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-icon-label.deprecated {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.66;
|
||||
}
|
||||
18
lib/vscode/src/vs/editor/contrib/documentSymbols/outline.ts
Normal file
18
lib/vscode/src/vs/editor/contrib/documentSymbols/outline.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
|
||||
export const OutlineViewId = 'outline';
|
||||
|
||||
export const OutlineViewFiltered = new RawContextKey('outlineFiltered', false);
|
||||
export const OutlineViewFocused = new RawContextKey('outlineFocused', false);
|
||||
|
||||
export const enum OutlineConfigKeys {
|
||||
'icons' = 'outline.icons',
|
||||
'problemsEnabled' = 'outline.problems.enabled',
|
||||
'problemsColors' = 'outline.problems.colors',
|
||||
'problemsBadges' = 'outline.problems.badges'
|
||||
}
|
||||
448
lib/vscode/src/vs/editor/contrib/documentSymbols/outlineModel.ts
Normal file
448
lib/vscode/src/vs/editor/contrib/documentSymbols/outlineModel.ts
Normal file
@@ -0,0 +1,448 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { binarySearch, coalesceInPlace, equals } from 'vs/base/common/arrays';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { LRUCache } from 'vs/base/common/map';
|
||||
import { commonPrefixLength } from 'vs/base/common/strings';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { DocumentSymbol, DocumentSymbolProvider, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { MarkerSeverity } from 'vs/platform/markers/common/markers';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry';
|
||||
|
||||
export abstract class TreeElement {
|
||||
|
||||
abstract id: string;
|
||||
abstract children: Map<string, TreeElement>;
|
||||
abstract parent: TreeElement | undefined;
|
||||
|
||||
abstract adopt(newParent: TreeElement): TreeElement;
|
||||
|
||||
remove(): void {
|
||||
if (this.parent) {
|
||||
this.parent.children.delete(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
static findId(candidate: DocumentSymbol | string, container: TreeElement): string {
|
||||
// complex id-computation which contains the origin/extension,
|
||||
// the parent path, and some dedupe logic when names collide
|
||||
let candidateId: string;
|
||||
if (typeof candidate === 'string') {
|
||||
candidateId = `${container.id}/${candidate}`;
|
||||
} else {
|
||||
candidateId = `${container.id}/${candidate.name}`;
|
||||
if (container.children.get(candidateId) !== undefined) {
|
||||
candidateId = `${container.id}/${candidate.name}_${candidate.range.startLineNumber}_${candidate.range.startColumn}`;
|
||||
}
|
||||
}
|
||||
|
||||
let id = candidateId;
|
||||
for (let i = 0; container.children.get(id) !== undefined; i++) {
|
||||
id = `${candidateId}_${i}`;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
static getElementById(id: string, element: TreeElement): TreeElement | undefined {
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
let len = commonPrefixLength(id, element.id);
|
||||
if (len === id.length) {
|
||||
return element;
|
||||
}
|
||||
if (len < element.id.length) {
|
||||
return undefined;
|
||||
}
|
||||
for (const [, child] of element.children) {
|
||||
let candidate = TreeElement.getElementById(id, child);
|
||||
if (candidate) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
static size(element: TreeElement): number {
|
||||
let res = 1;
|
||||
for (const [, child] of element.children) {
|
||||
res += TreeElement.size(child);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
static empty(element: TreeElement): boolean {
|
||||
return element.children.size === 0;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IOutlineMarker {
|
||||
startLineNumber: number;
|
||||
startColumn: number;
|
||||
endLineNumber: number;
|
||||
endColumn: number;
|
||||
severity: MarkerSeverity;
|
||||
}
|
||||
|
||||
export class OutlineElement extends TreeElement {
|
||||
|
||||
children = new Map<string, OutlineElement>();
|
||||
marker: { count: number, topSev: MarkerSeverity } | undefined;
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
public parent: TreeElement | undefined,
|
||||
readonly symbol: DocumentSymbol
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
adopt(parent: TreeElement): OutlineElement {
|
||||
let res = new OutlineElement(this.id, parent, this.symbol);
|
||||
for (const [key, value] of this.children) {
|
||||
res.children.set(key, value.adopt(res));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlineGroup extends TreeElement {
|
||||
|
||||
children = new Map<string, OutlineElement>();
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
public parent: TreeElement | undefined,
|
||||
readonly label: string,
|
||||
readonly order: number,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
adopt(parent: TreeElement): OutlineGroup {
|
||||
let res = new OutlineGroup(this.id, parent, this.label, this.order);
|
||||
for (const [key, value] of this.children) {
|
||||
res.children.set(key, value.adopt(res));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
getItemEnclosingPosition(position: IPosition): OutlineElement | undefined {
|
||||
return position ? this._getItemEnclosingPosition(position, this.children) : undefined;
|
||||
}
|
||||
|
||||
private _getItemEnclosingPosition(position: IPosition, children: Map<string, OutlineElement>): OutlineElement | undefined {
|
||||
for (const [, item] of children) {
|
||||
if (!item.symbol.range || !Range.containsPosition(item.symbol.range, position)) {
|
||||
continue;
|
||||
}
|
||||
return this._getItemEnclosingPosition(position, item.children) || item;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
updateMarker(marker: IOutlineMarker[]): void {
|
||||
for (const [, child] of this.children) {
|
||||
this._updateMarker(marker, child);
|
||||
}
|
||||
}
|
||||
|
||||
private _updateMarker(markers: IOutlineMarker[], item: OutlineElement): void {
|
||||
item.marker = undefined;
|
||||
|
||||
// find the proper start index to check for item/marker overlap.
|
||||
let idx = binarySearch<IRange>(markers, item.symbol.range, Range.compareRangesUsingStarts);
|
||||
let start: number;
|
||||
if (idx < 0) {
|
||||
start = ~idx;
|
||||
if (start > 0 && Range.areIntersecting(markers[start - 1], item.symbol.range)) {
|
||||
start -= 1;
|
||||
}
|
||||
} else {
|
||||
start = idx;
|
||||
}
|
||||
|
||||
let myMarkers: IOutlineMarker[] = [];
|
||||
let myTopSev: MarkerSeverity | undefined;
|
||||
|
||||
for (; start < markers.length && Range.areIntersecting(item.symbol.range, markers[start]); start++) {
|
||||
// remove markers intersecting with this outline element
|
||||
// and store them in a 'private' array.
|
||||
let marker = markers[start];
|
||||
myMarkers.push(marker);
|
||||
(markers as Array<IOutlineMarker | undefined>)[start] = undefined;
|
||||
if (!myTopSev || marker.severity > myTopSev) {
|
||||
myTopSev = marker.severity;
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse into children and let them match markers that have matched
|
||||
// this outline element. This might remove markers from this element and
|
||||
// therefore we remember that we have had markers. That allows us to render
|
||||
// the dot, saying 'this element has children with markers'
|
||||
for (const [, child] of item.children) {
|
||||
this._updateMarker(myMarkers, child);
|
||||
}
|
||||
|
||||
if (myTopSev) {
|
||||
item.marker = {
|
||||
count: myMarkers.length,
|
||||
topSev: myTopSev
|
||||
};
|
||||
}
|
||||
|
||||
coalesceInPlace(markers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class OutlineModel extends TreeElement {
|
||||
|
||||
private static readonly _requestDurations = new LanguageFeatureRequestDelays(DocumentSymbolProviderRegistry, 350);
|
||||
private static readonly _requests = new LRUCache<string, { promiseCnt: number, source: CancellationTokenSource, promise: Promise<any>, model: OutlineModel | undefined }>(9, 0.75);
|
||||
private static readonly _keys = new class {
|
||||
|
||||
private _counter = 1;
|
||||
private _data = new WeakMap<DocumentSymbolProvider, number>();
|
||||
|
||||
for(textModel: ITextModel, version: boolean): string {
|
||||
return `${textModel.id}/${version ? textModel.getVersionId() : ''}/${this._hash(DocumentSymbolProviderRegistry.all(textModel))}`;
|
||||
}
|
||||
|
||||
private _hash(providers: DocumentSymbolProvider[]): string {
|
||||
let result = '';
|
||||
for (const provider of providers) {
|
||||
let n = this._data.get(provider);
|
||||
if (typeof n === 'undefined') {
|
||||
n = this._counter++;
|
||||
this._data.set(provider, n);
|
||||
}
|
||||
result += n;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
static create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
|
||||
|
||||
let key = this._keys.for(textModel, true);
|
||||
let data = OutlineModel._requests.get(key);
|
||||
|
||||
if (!data) {
|
||||
let source = new CancellationTokenSource();
|
||||
data = {
|
||||
promiseCnt: 0,
|
||||
source,
|
||||
promise: OutlineModel._create(textModel, source.token),
|
||||
model: undefined,
|
||||
};
|
||||
OutlineModel._requests.set(key, data);
|
||||
|
||||
// keep moving average of request durations
|
||||
const now = Date.now();
|
||||
data.promise.then(() => {
|
||||
this._requestDurations.update(textModel, Date.now() - now);
|
||||
});
|
||||
}
|
||||
|
||||
if (data!.model) {
|
||||
// resolved -> return data
|
||||
return Promise.resolve(data.model!);
|
||||
}
|
||||
|
||||
// increase usage counter
|
||||
data!.promiseCnt += 1;
|
||||
|
||||
token.onCancellationRequested(() => {
|
||||
// last -> cancel provider request, remove cached promise
|
||||
if (--data!.promiseCnt === 0) {
|
||||
data!.source.cancel();
|
||||
OutlineModel._requests.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
data!.promise.then(model => {
|
||||
data!.model = model;
|
||||
resolve(model);
|
||||
}, err => {
|
||||
OutlineModel._requests.delete(key);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
static getRequestDelay(textModel: ITextModel | null): number {
|
||||
return textModel ? this._requestDurations.get(textModel) : this._requestDurations.min;
|
||||
}
|
||||
|
||||
private static _create(textModel: ITextModel, token: CancellationToken): Promise<OutlineModel> {
|
||||
|
||||
const cts = new CancellationTokenSource(token);
|
||||
const result = new OutlineModel(textModel.uri);
|
||||
const provider = DocumentSymbolProviderRegistry.ordered(textModel);
|
||||
const promises = provider.map((provider, index) => {
|
||||
|
||||
let id = TreeElement.findId(`provider_${index}`, result);
|
||||
let group = new OutlineGroup(id, result, provider.displayName ?? 'Unknown Outline Provider', index);
|
||||
|
||||
return Promise.resolve(provider.provideDocumentSymbols(textModel, cts.token)).then(result => {
|
||||
for (const info of result || []) {
|
||||
OutlineModel._makeOutlineElement(info, group);
|
||||
}
|
||||
return group;
|
||||
}, err => {
|
||||
onUnexpectedExternalError(err);
|
||||
return group;
|
||||
}).then(group => {
|
||||
if (!TreeElement.empty(group)) {
|
||||
result._groups.set(id, group);
|
||||
} else {
|
||||
group.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const listener = DocumentSymbolProviderRegistry.onDidChange(() => {
|
||||
const newProvider = DocumentSymbolProviderRegistry.ordered(textModel);
|
||||
if (!equals(newProvider, provider)) {
|
||||
cts.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
if (cts.token.isCancellationRequested && !token.isCancellationRequested) {
|
||||
return OutlineModel._create(textModel, token);
|
||||
} else {
|
||||
return result._compact();
|
||||
}
|
||||
}).finally(() => {
|
||||
listener.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
private static _makeOutlineElement(info: DocumentSymbol, container: OutlineGroup | OutlineElement): void {
|
||||
let id = TreeElement.findId(info, container);
|
||||
let res = new OutlineElement(id, container, info);
|
||||
if (info.children) {
|
||||
for (const childInfo of info.children) {
|
||||
OutlineModel._makeOutlineElement(childInfo, res);
|
||||
}
|
||||
}
|
||||
container.children.set(res.id, res);
|
||||
}
|
||||
|
||||
static get(element: TreeElement | undefined): OutlineModel | undefined {
|
||||
while (element) {
|
||||
if (element instanceof OutlineModel) {
|
||||
return element;
|
||||
}
|
||||
element = element.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
readonly id = 'root';
|
||||
readonly parent = undefined;
|
||||
|
||||
protected _groups = new Map<string, OutlineGroup>();
|
||||
children = new Map<string, OutlineGroup | OutlineElement>();
|
||||
|
||||
protected constructor(readonly uri: URI) {
|
||||
super();
|
||||
|
||||
this.id = 'root';
|
||||
this.parent = undefined;
|
||||
}
|
||||
|
||||
adopt(): OutlineModel {
|
||||
let res = new OutlineModel(this.uri);
|
||||
for (const [key, value] of this._groups) {
|
||||
res._groups.set(key, value.adopt(res));
|
||||
}
|
||||
return res._compact();
|
||||
}
|
||||
|
||||
private _compact(): this {
|
||||
let count = 0;
|
||||
for (const [key, group] of this._groups) {
|
||||
if (group.children.size === 0) { // empty
|
||||
this._groups.delete(key);
|
||||
} else {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
if (count !== 1) {
|
||||
//
|
||||
this.children = this._groups;
|
||||
} else {
|
||||
// adopt all elements of the first group
|
||||
let group = Iterable.first(this._groups.values())!;
|
||||
for (let [, child] of group.children) {
|
||||
child.parent = this;
|
||||
this.children.set(child.id, child);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
merge(other: OutlineModel): boolean {
|
||||
if (this.uri.toString() !== other.uri.toString()) {
|
||||
return false;
|
||||
}
|
||||
if (this._groups.size !== other._groups.size) {
|
||||
return false;
|
||||
}
|
||||
this._groups = other._groups;
|
||||
this.children = other.children;
|
||||
return true;
|
||||
}
|
||||
|
||||
getItemEnclosingPosition(position: IPosition, context?: OutlineElement): OutlineElement | undefined {
|
||||
|
||||
let preferredGroup: OutlineGroup | undefined;
|
||||
if (context) {
|
||||
let candidate = context.parent;
|
||||
while (candidate && !preferredGroup) {
|
||||
if (candidate instanceof OutlineGroup) {
|
||||
preferredGroup = candidate;
|
||||
}
|
||||
candidate = candidate.parent;
|
||||
}
|
||||
}
|
||||
|
||||
let result: OutlineElement | undefined = undefined;
|
||||
for (const [, group] of this._groups) {
|
||||
result = group.getItemEnclosingPosition(position);
|
||||
if (result && (!preferredGroup || preferredGroup === group)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getItemById(id: string): TreeElement | undefined {
|
||||
return TreeElement.getElementById(id, this);
|
||||
}
|
||||
|
||||
updateMarker(marker: IOutlineMarker[]): void {
|
||||
// sort markers by start range so that we can use
|
||||
// outline element starts for quicker look up
|
||||
marker.sort(Range.compareRangesUsingStarts);
|
||||
|
||||
for (const [, group] of this._groups) {
|
||||
group.updateMarker(marker.slice(0));
|
||||
}
|
||||
}
|
||||
}
|
||||
725
lib/vscode/src/vs/editor/contrib/documentSymbols/outlineTree.ts
Normal file
725
lib/vscode/src/vs/editor/contrib/documentSymbols/outlineTree.ts
Normal file
@@ -0,0 +1,725 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
import { IIdentityProvider, IKeyboardNavigationLabelProvider, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
|
||||
import { IDataSource, ITreeNode, ITreeRenderer, ITreeSorter, ITreeFilter } from 'vs/base/browser/ui/tree/tree';
|
||||
import { createMatches, FuzzyScore } from 'vs/base/common/filters';
|
||||
import 'vs/css!./media/outlineTree';
|
||||
import 'vs/css!./media/symbol-icons';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { SymbolKind, SymbolKinds, SymbolTag } from 'vs/editor/common/modes';
|
||||
import { OutlineElement, OutlineGroup, OutlineModel } from 'vs/editor/contrib/documentSymbols/outlineModel';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { OutlineConfigKeys } from 'vs/editor/contrib/documentSymbols/outline';
|
||||
import { MarkerSeverity } from 'vs/platform/markers/common/markers';
|
||||
import { IThemeService, registerThemingParticipant, IColorTheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService';
|
||||
import { registerColor, listErrorForeground, listWarningForeground, foreground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IdleValue } from 'vs/base/common/async';
|
||||
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfigurationService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
export type OutlineItem = OutlineGroup | OutlineElement;
|
||||
|
||||
export class OutlineNavigationLabelProvider implements IKeyboardNavigationLabelProvider<OutlineItem> {
|
||||
|
||||
getKeyboardNavigationLabel(element: OutlineItem): { toString(): string; } {
|
||||
if (element instanceof OutlineGroup) {
|
||||
return element.label;
|
||||
} else {
|
||||
return element.symbol.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlineAccessibilityProvider implements IListAccessibilityProvider<OutlineItem> {
|
||||
|
||||
constructor(private readonly ariaLabel: string) { }
|
||||
|
||||
getWidgetAriaLabel(): string {
|
||||
return this.ariaLabel;
|
||||
}
|
||||
|
||||
getAriaLabel(element: OutlineItem): string | null {
|
||||
if (element instanceof OutlineGroup) {
|
||||
return element.label;
|
||||
} else {
|
||||
return element.symbol.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlineIdentityProvider implements IIdentityProvider<OutlineItem> {
|
||||
getId(element: OutlineItem): { toString(): string; } {
|
||||
return element.id;
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlineGroupTemplate {
|
||||
static readonly id = 'OutlineGroupTemplate';
|
||||
constructor(
|
||||
readonly labelContainer: HTMLElement,
|
||||
readonly label: HighlightedLabel,
|
||||
) { }
|
||||
}
|
||||
|
||||
export class OutlineElementTemplate {
|
||||
static readonly id = 'OutlineElementTemplate';
|
||||
constructor(
|
||||
readonly container: HTMLElement,
|
||||
readonly iconLabel: IconLabel,
|
||||
readonly iconClass: HTMLElement,
|
||||
readonly decoration: HTMLElement,
|
||||
) { }
|
||||
}
|
||||
|
||||
export class OutlineVirtualDelegate implements IListVirtualDelegate<OutlineItem> {
|
||||
|
||||
getHeight(_element: OutlineItem): number {
|
||||
return 22;
|
||||
}
|
||||
|
||||
getTemplateId(element: OutlineItem): string {
|
||||
if (element instanceof OutlineGroup) {
|
||||
return OutlineGroupTemplate.id;
|
||||
} else {
|
||||
return OutlineElementTemplate.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlineGroupRenderer implements ITreeRenderer<OutlineGroup, FuzzyScore, OutlineGroupTemplate> {
|
||||
|
||||
readonly templateId: string = OutlineGroupTemplate.id;
|
||||
|
||||
renderTemplate(container: HTMLElement): OutlineGroupTemplate {
|
||||
const labelContainer = dom.$('.outline-element-label');
|
||||
container.classList.add('outline-element');
|
||||
dom.append(container, labelContainer);
|
||||
return new OutlineGroupTemplate(labelContainer, new HighlightedLabel(labelContainer, true));
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<OutlineGroup, FuzzyScore>, index: number, template: OutlineGroupTemplate): void {
|
||||
template.label.set(
|
||||
node.element.label,
|
||||
createMatches(node.filterData)
|
||||
);
|
||||
}
|
||||
|
||||
disposeTemplate(_template: OutlineGroupTemplate): void {
|
||||
// nothing
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlineElementRenderer implements ITreeRenderer<OutlineElement, FuzzyScore, OutlineElementTemplate> {
|
||||
|
||||
readonly templateId: string = OutlineElementTemplate.id;
|
||||
|
||||
constructor(
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IThemeService private readonly _themeService: IThemeService,
|
||||
) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): OutlineElementTemplate {
|
||||
container.classList.add('outline-element');
|
||||
const iconLabel = new IconLabel(container, { supportHighlights: true });
|
||||
const iconClass = dom.$('.outline-element-icon');
|
||||
const decoration = dom.$('.outline-element-decoration');
|
||||
container.prepend(iconClass);
|
||||
container.appendChild(decoration);
|
||||
return new OutlineElementTemplate(container, iconLabel, iconClass, decoration);
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<OutlineElement, FuzzyScore>, index: number, template: OutlineElementTemplate): void {
|
||||
const { element } = node;
|
||||
const options = {
|
||||
matches: createMatches(node.filterData),
|
||||
labelEscapeNewLines: true,
|
||||
extraClasses: <string[]>[],
|
||||
title: localize('title.template', "{0} ({1})", element.symbol.name, OutlineElementRenderer._symbolKindNames[element.symbol.kind])
|
||||
};
|
||||
if (this._configurationService.getValue(OutlineConfigKeys.icons)) {
|
||||
// add styles for the icons
|
||||
template.iconClass.className = '';
|
||||
template.iconClass.classList.add(`outline-element-icon`, ...SymbolKinds.toCssClassName(element.symbol.kind, true).split(' '));
|
||||
}
|
||||
if (element.symbol.tags.indexOf(SymbolTag.Deprecated) >= 0) {
|
||||
options.extraClasses.push(`deprecated`);
|
||||
options.matches = [];
|
||||
}
|
||||
template.iconLabel.setLabel(element.symbol.name, element.symbol.detail, options);
|
||||
this._renderMarkerInfo(element, template);
|
||||
}
|
||||
|
||||
private _renderMarkerInfo(element: OutlineElement, template: OutlineElementTemplate): void {
|
||||
|
||||
if (!element.marker) {
|
||||
dom.hide(template.decoration);
|
||||
template.container.style.removeProperty('--outline-element-color');
|
||||
return;
|
||||
}
|
||||
|
||||
const { count, topSev } = element.marker;
|
||||
const color = this._themeService.getColorTheme().getColor(topSev === MarkerSeverity.Error ? listErrorForeground : listWarningForeground);
|
||||
const cssColor = color ? color.toString() : 'inherit';
|
||||
|
||||
// color of the label
|
||||
if (this._configurationService.getValue(OutlineConfigKeys.problemsColors)) {
|
||||
template.container.style.setProperty('--outline-element-color', cssColor);
|
||||
} else {
|
||||
template.container.style.removeProperty('--outline-element-color');
|
||||
}
|
||||
|
||||
// badge with color/rollup
|
||||
if (!this._configurationService.getValue(OutlineConfigKeys.problemsBadges)) {
|
||||
dom.hide(template.decoration);
|
||||
|
||||
} else if (count > 0) {
|
||||
dom.show(template.decoration);
|
||||
template.decoration.classList.remove('bubble');
|
||||
template.decoration.innerText = count < 10 ? count.toString() : '+9';
|
||||
template.decoration.title = count === 1 ? localize('1.problem', "1 problem in this element") : localize('N.problem', "{0} problems in this element", count);
|
||||
template.decoration.style.setProperty('--outline-element-color', cssColor);
|
||||
|
||||
} else {
|
||||
dom.show(template.decoration);
|
||||
template.decoration.classList.add('bubble');
|
||||
template.decoration.innerText = '\uea71';
|
||||
template.decoration.title = localize('deep.problem', "Contains elements with problems");
|
||||
template.decoration.style.setProperty('--outline-element-color', cssColor);
|
||||
}
|
||||
}
|
||||
|
||||
private static _symbolKindNames: { [symbol: number]: string } = {
|
||||
[SymbolKind.Array]: localize('Array', "array"),
|
||||
[SymbolKind.Boolean]: localize('Boolean', "boolean"),
|
||||
[SymbolKind.Class]: localize('Class', "class"),
|
||||
[SymbolKind.Constant]: localize('Constant', "constant"),
|
||||
[SymbolKind.Constructor]: localize('Constructor', "constructor"),
|
||||
[SymbolKind.Enum]: localize('Enum', "enumeration"),
|
||||
[SymbolKind.EnumMember]: localize('EnumMember', "enumeration member"),
|
||||
[SymbolKind.Event]: localize('Event', "event"),
|
||||
[SymbolKind.Field]: localize('Field', "field"),
|
||||
[SymbolKind.File]: localize('File', "file"),
|
||||
[SymbolKind.Function]: localize('Function', "function"),
|
||||
[SymbolKind.Interface]: localize('Interface', "interface"),
|
||||
[SymbolKind.Key]: localize('Key', "key"),
|
||||
[SymbolKind.Method]: localize('Method', "method"),
|
||||
[SymbolKind.Module]: localize('Module', "module"),
|
||||
[SymbolKind.Namespace]: localize('Namespace', "namespace"),
|
||||
[SymbolKind.Null]: localize('Null', "null"),
|
||||
[SymbolKind.Number]: localize('Number', "number"),
|
||||
[SymbolKind.Object]: localize('Object', "object"),
|
||||
[SymbolKind.Operator]: localize('Operator', "operator"),
|
||||
[SymbolKind.Package]: localize('Package', "package"),
|
||||
[SymbolKind.Property]: localize('Property', "property"),
|
||||
[SymbolKind.String]: localize('String', "string"),
|
||||
[SymbolKind.Struct]: localize('Struct', "struct"),
|
||||
[SymbolKind.TypeParameter]: localize('TypeParameter', "type parameter"),
|
||||
[SymbolKind.Variable]: localize('Variable', "variable"),
|
||||
};
|
||||
|
||||
disposeTemplate(_template: OutlineElementTemplate): void {
|
||||
_template.iconLabel.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export const enum OutlineSortOrder {
|
||||
ByPosition,
|
||||
ByName,
|
||||
ByKind
|
||||
}
|
||||
|
||||
export class OutlineFilter implements ITreeFilter<OutlineItem> {
|
||||
|
||||
static readonly configNameToKind = Object.freeze({
|
||||
['showFiles']: SymbolKind.File,
|
||||
['showModules']: SymbolKind.Module,
|
||||
['showNamespaces']: SymbolKind.Namespace,
|
||||
['showPackages']: SymbolKind.Package,
|
||||
['showClasses']: SymbolKind.Class,
|
||||
['showMethods']: SymbolKind.Method,
|
||||
['showProperties']: SymbolKind.Property,
|
||||
['showFields']: SymbolKind.Field,
|
||||
['showConstructors']: SymbolKind.Constructor,
|
||||
['showEnums']: SymbolKind.Enum,
|
||||
['showInterfaces']: SymbolKind.Interface,
|
||||
['showFunctions']: SymbolKind.Function,
|
||||
['showVariables']: SymbolKind.Variable,
|
||||
['showConstants']: SymbolKind.Constant,
|
||||
['showStrings']: SymbolKind.String,
|
||||
['showNumbers']: SymbolKind.Number,
|
||||
['showBooleans']: SymbolKind.Boolean,
|
||||
['showArrays']: SymbolKind.Array,
|
||||
['showObjects']: SymbolKind.Object,
|
||||
['showKeys']: SymbolKind.Key,
|
||||
['showNull']: SymbolKind.Null,
|
||||
['showEnumMembers']: SymbolKind.EnumMember,
|
||||
['showStructs']: SymbolKind.Struct,
|
||||
['showEvents']: SymbolKind.Event,
|
||||
['showOperators']: SymbolKind.Operator,
|
||||
['showTypeParameters']: SymbolKind.TypeParameter,
|
||||
});
|
||||
|
||||
static readonly kindToConfigName = Object.freeze({
|
||||
[SymbolKind.File]: 'showFiles',
|
||||
[SymbolKind.Module]: 'showModules',
|
||||
[SymbolKind.Namespace]: 'showNamespaces',
|
||||
[SymbolKind.Package]: 'showPackages',
|
||||
[SymbolKind.Class]: 'showClasses',
|
||||
[SymbolKind.Method]: 'showMethods',
|
||||
[SymbolKind.Property]: 'showProperties',
|
||||
[SymbolKind.Field]: 'showFields',
|
||||
[SymbolKind.Constructor]: 'showConstructors',
|
||||
[SymbolKind.Enum]: 'showEnums',
|
||||
[SymbolKind.Interface]: 'showInterfaces',
|
||||
[SymbolKind.Function]: 'showFunctions',
|
||||
[SymbolKind.Variable]: 'showVariables',
|
||||
[SymbolKind.Constant]: 'showConstants',
|
||||
[SymbolKind.String]: 'showStrings',
|
||||
[SymbolKind.Number]: 'showNumbers',
|
||||
[SymbolKind.Boolean]: 'showBooleans',
|
||||
[SymbolKind.Array]: 'showArrays',
|
||||
[SymbolKind.Object]: 'showObjects',
|
||||
[SymbolKind.Key]: 'showKeys',
|
||||
[SymbolKind.Null]: 'showNull',
|
||||
[SymbolKind.EnumMember]: 'showEnumMembers',
|
||||
[SymbolKind.Struct]: 'showStructs',
|
||||
[SymbolKind.Event]: 'showEvents',
|
||||
[SymbolKind.Operator]: 'showOperators',
|
||||
[SymbolKind.TypeParameter]: 'showTypeParameters',
|
||||
});
|
||||
|
||||
constructor(
|
||||
private readonly _prefix: string,
|
||||
@ITextResourceConfigurationService private readonly _textResourceConfigService: ITextResourceConfigurationService,
|
||||
) { }
|
||||
|
||||
filter(element: OutlineItem): boolean {
|
||||
const outline = OutlineModel.get(element);
|
||||
let uri: URI | undefined;
|
||||
|
||||
if (outline) {
|
||||
uri = outline.uri;
|
||||
}
|
||||
|
||||
if (!(element instanceof OutlineElement)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const configName = OutlineFilter.kindToConfigName[element.symbol.kind];
|
||||
const configKey = `${this._prefix}.${configName}`;
|
||||
return this._textResourceConfigService.getValue(uri, configKey);
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlineItemComparator implements ITreeSorter<OutlineItem> {
|
||||
|
||||
private readonly _collator = new IdleValue<Intl.Collator>(() => new Intl.Collator(undefined, { numeric: true }));
|
||||
|
||||
constructor(
|
||||
public type: OutlineSortOrder = OutlineSortOrder.ByPosition
|
||||
) { }
|
||||
|
||||
compare(a: OutlineItem, b: OutlineItem): number {
|
||||
if (a instanceof OutlineGroup && b instanceof OutlineGroup) {
|
||||
return a.order - b.order;
|
||||
|
||||
} else if (a instanceof OutlineElement && b instanceof OutlineElement) {
|
||||
if (this.type === OutlineSortOrder.ByKind) {
|
||||
return a.symbol.kind - b.symbol.kind || this._collator.value.compare(a.symbol.name, b.symbol.name);
|
||||
} else if (this.type === OutlineSortOrder.ByName) {
|
||||
return this._collator.value.compare(a.symbol.name, b.symbol.name) || Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range);
|
||||
} else if (this.type === OutlineSortOrder.ByPosition) {
|
||||
return Range.compareRangesUsingStarts(a.symbol.range, b.symbol.range) || this._collator.value.compare(a.symbol.name, b.symbol.name);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class OutlineDataSource implements IDataSource<OutlineModel, OutlineItem> {
|
||||
|
||||
getChildren(element: undefined | OutlineModel | OutlineGroup | OutlineElement) {
|
||||
if (!element) {
|
||||
return Iterable.empty();
|
||||
}
|
||||
return element.children.values();
|
||||
}
|
||||
}
|
||||
|
||||
export const SYMBOL_ICON_ARRAY_FOREGROUND = registerColor('symbolIcon.arrayForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.arrayForeground', 'The foreground color for array symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_BOOLEAN_FOREGROUND = registerColor('symbolIcon.booleanForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.booleanForeground', 'The foreground color for boolean symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_CLASS_FOREGROUND = registerColor('symbolIcon.classForeground', {
|
||||
dark: '#EE9D28',
|
||||
light: '#D67E00',
|
||||
hc: '#EE9D28'
|
||||
}, localize('symbolIcon.classForeground', 'The foreground color for class symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_COLOR_FOREGROUND = registerColor('symbolIcon.colorForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.colorForeground', 'The foreground color for color symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_CONSTANT_FOREGROUND = registerColor('symbolIcon.constantForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.constantForeground', 'The foreground color for constant symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_CONSTRUCTOR_FOREGROUND = registerColor('symbolIcon.constructorForeground', {
|
||||
dark: '#B180D7',
|
||||
light: '#652D90',
|
||||
hc: '#B180D7'
|
||||
}, localize('symbolIcon.constructorForeground', 'The foreground color for constructor symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_ENUMERATOR_FOREGROUND = registerColor('symbolIcon.enumeratorForeground', {
|
||||
dark: '#EE9D28',
|
||||
light: '#D67E00',
|
||||
hc: '#EE9D28'
|
||||
}, localize('symbolIcon.enumeratorForeground', 'The foreground color for enumerator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_ENUMERATOR_MEMBER_FOREGROUND = registerColor('symbolIcon.enumeratorMemberForeground', {
|
||||
dark: '#75BEFF',
|
||||
light: '#007ACC',
|
||||
hc: '#75BEFF'
|
||||
}, localize('symbolIcon.enumeratorMemberForeground', 'The foreground color for enumerator member symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_EVENT_FOREGROUND = registerColor('symbolIcon.eventForeground', {
|
||||
dark: '#EE9D28',
|
||||
light: '#D67E00',
|
||||
hc: '#EE9D28'
|
||||
}, localize('symbolIcon.eventForeground', 'The foreground color for event symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_FIELD_FOREGROUND = registerColor('symbolIcon.fieldForeground', {
|
||||
dark: '#75BEFF',
|
||||
light: '#007ACC',
|
||||
hc: '#75BEFF'
|
||||
}, localize('symbolIcon.fieldForeground', 'The foreground color for field symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_FILE_FOREGROUND = registerColor('symbolIcon.fileForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.fileForeground', 'The foreground color for file symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_FOLDER_FOREGROUND = registerColor('symbolIcon.folderForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.folderForeground', 'The foreground color for folder symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_FUNCTION_FOREGROUND = registerColor('symbolIcon.functionForeground', {
|
||||
dark: '#B180D7',
|
||||
light: '#652D90',
|
||||
hc: '#B180D7'
|
||||
}, localize('symbolIcon.functionForeground', 'The foreground color for function symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_INTERFACE_FOREGROUND = registerColor('symbolIcon.interfaceForeground', {
|
||||
dark: '#75BEFF',
|
||||
light: '#007ACC',
|
||||
hc: '#75BEFF'
|
||||
}, localize('symbolIcon.interfaceForeground', 'The foreground color for interface symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_KEY_FOREGROUND = registerColor('symbolIcon.keyForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.keyForeground', 'The foreground color for key symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_KEYWORD_FOREGROUND = registerColor('symbolIcon.keywordForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.keywordForeground', 'The foreground color for keyword symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_METHOD_FOREGROUND = registerColor('symbolIcon.methodForeground', {
|
||||
dark: '#B180D7',
|
||||
light: '#652D90',
|
||||
hc: '#B180D7'
|
||||
}, localize('symbolIcon.methodForeground', 'The foreground color for method symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_MODULE_FOREGROUND = registerColor('symbolIcon.moduleForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.moduleForeground', 'The foreground color for module symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_NAMESPACE_FOREGROUND = registerColor('symbolIcon.namespaceForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.namespaceForeground', 'The foreground color for namespace symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_NULL_FOREGROUND = registerColor('symbolIcon.nullForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.nullForeground', 'The foreground color for null symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_NUMBER_FOREGROUND = registerColor('symbolIcon.numberForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.numberForeground', 'The foreground color for number symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_OBJECT_FOREGROUND = registerColor('symbolIcon.objectForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.objectForeground', 'The foreground color for object symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_OPERATOR_FOREGROUND = registerColor('symbolIcon.operatorForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.operatorForeground', 'The foreground color for operator symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_PACKAGE_FOREGROUND = registerColor('symbolIcon.packageForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.packageForeground', 'The foreground color for package symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_PROPERTY_FOREGROUND = registerColor('symbolIcon.propertyForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.propertyForeground', 'The foreground color for property symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_REFERENCE_FOREGROUND = registerColor('symbolIcon.referenceForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.referenceForeground', 'The foreground color for reference symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_SNIPPET_FOREGROUND = registerColor('symbolIcon.snippetForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.snippetForeground', 'The foreground color for snippet symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_STRING_FOREGROUND = registerColor('symbolIcon.stringForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.stringForeground', 'The foreground color for string symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_STRUCT_FOREGROUND = registerColor('symbolIcon.structForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.structForeground', 'The foreground color for struct symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_TEXT_FOREGROUND = registerColor('symbolIcon.textForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.textForeground', 'The foreground color for text symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_TYPEPARAMETER_FOREGROUND = registerColor('symbolIcon.typeParameterForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.typeParameterForeground', 'The foreground color for type parameter symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_UNIT_FOREGROUND = registerColor('symbolIcon.unitForeground', {
|
||||
dark: foreground,
|
||||
light: foreground,
|
||||
hc: foreground
|
||||
}, localize('symbolIcon.unitForeground', 'The foreground color for unit symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
export const SYMBOL_ICON_VARIABLE_FOREGROUND = registerColor('symbolIcon.variableForeground', {
|
||||
dark: '#75BEFF',
|
||||
light: '#007ACC',
|
||||
hc: '#75BEFF'
|
||||
}, localize('symbolIcon.variableForeground', 'The foreground color for variable symbols. These symbols appear in the outline, breadcrumb, and suggest widget.'));
|
||||
|
||||
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
|
||||
|
||||
const symbolIconArrayColor = theme.getColor(SYMBOL_ICON_ARRAY_FOREGROUND);
|
||||
if (symbolIconArrayColor) {
|
||||
collector.addRule(`${Codicon.symbolArray.cssSelector} { color: ${symbolIconArrayColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconBooleanColor = theme.getColor(SYMBOL_ICON_BOOLEAN_FOREGROUND);
|
||||
if (symbolIconBooleanColor) {
|
||||
collector.addRule(`${Codicon.symbolBoolean.cssSelector} { color: ${symbolIconBooleanColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconClassColor = theme.getColor(SYMBOL_ICON_CLASS_FOREGROUND);
|
||||
if (symbolIconClassColor) {
|
||||
collector.addRule(`${Codicon.symbolClass.cssSelector} { color: ${symbolIconClassColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconMethodColor = theme.getColor(SYMBOL_ICON_METHOD_FOREGROUND);
|
||||
if (symbolIconMethodColor) {
|
||||
collector.addRule(`${Codicon.symbolMethod.cssSelector} { color: ${symbolIconMethodColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconColorColor = theme.getColor(SYMBOL_ICON_COLOR_FOREGROUND);
|
||||
if (symbolIconColorColor) {
|
||||
collector.addRule(`${Codicon.symbolColor.cssSelector} { color: ${symbolIconColorColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconConstantColor = theme.getColor(SYMBOL_ICON_CONSTANT_FOREGROUND);
|
||||
if (symbolIconConstantColor) {
|
||||
collector.addRule(`${Codicon.symbolConstant.cssSelector} { color: ${symbolIconConstantColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconConstructorColor = theme.getColor(SYMBOL_ICON_CONSTRUCTOR_FOREGROUND);
|
||||
if (symbolIconConstructorColor) {
|
||||
collector.addRule(`${Codicon.symbolConstructor.cssSelector} { color: ${symbolIconConstructorColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconEnumeratorColor = theme.getColor(SYMBOL_ICON_ENUMERATOR_FOREGROUND);
|
||||
if (symbolIconEnumeratorColor) {
|
||||
collector.addRule(`
|
||||
${Codicon.symbolValue.cssSelector},${Codicon.symbolEnum.cssSelector} { color: ${symbolIconEnumeratorColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconEnumeratorMemberColor = theme.getColor(SYMBOL_ICON_ENUMERATOR_MEMBER_FOREGROUND);
|
||||
if (symbolIconEnumeratorMemberColor) {
|
||||
collector.addRule(`${Codicon.symbolEnumMember.cssSelector} { color: ${symbolIconEnumeratorMemberColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconEventColor = theme.getColor(SYMBOL_ICON_EVENT_FOREGROUND);
|
||||
if (symbolIconEventColor) {
|
||||
collector.addRule(`${Codicon.symbolEvent.cssSelector} { color: ${symbolIconEventColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconFieldColor = theme.getColor(SYMBOL_ICON_FIELD_FOREGROUND);
|
||||
if (symbolIconFieldColor) {
|
||||
collector.addRule(`${Codicon.symbolField.cssSelector} { color: ${symbolIconFieldColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconFileColor = theme.getColor(SYMBOL_ICON_FILE_FOREGROUND);
|
||||
if (symbolIconFileColor) {
|
||||
collector.addRule(`${Codicon.symbolFile.cssSelector} { color: ${symbolIconFileColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconFolderColor = theme.getColor(SYMBOL_ICON_FOLDER_FOREGROUND);
|
||||
if (symbolIconFolderColor) {
|
||||
collector.addRule(`${Codicon.symbolFolder.cssSelector} { color: ${symbolIconFolderColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconFunctionColor = theme.getColor(SYMBOL_ICON_FUNCTION_FOREGROUND);
|
||||
if (symbolIconFunctionColor) {
|
||||
collector.addRule(`${Codicon.symbolFunction.cssSelector} { color: ${symbolIconFunctionColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconInterfaceColor = theme.getColor(SYMBOL_ICON_INTERFACE_FOREGROUND);
|
||||
if (symbolIconInterfaceColor) {
|
||||
collector.addRule(`${Codicon.symbolInterface.cssSelector} { color: ${symbolIconInterfaceColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconKeyColor = theme.getColor(SYMBOL_ICON_KEY_FOREGROUND);
|
||||
if (symbolIconKeyColor) {
|
||||
collector.addRule(`${Codicon.symbolKey.cssSelector} { color: ${symbolIconKeyColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconKeywordColor = theme.getColor(SYMBOL_ICON_KEYWORD_FOREGROUND);
|
||||
if (symbolIconKeywordColor) {
|
||||
collector.addRule(`${Codicon.symbolKeyword.cssSelector} { color: ${symbolIconKeywordColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconModuleColor = theme.getColor(SYMBOL_ICON_MODULE_FOREGROUND);
|
||||
if (symbolIconModuleColor) {
|
||||
collector.addRule(`${Codicon.symbolModule.cssSelector} { color: ${symbolIconModuleColor}; }`);
|
||||
}
|
||||
|
||||
const outlineNamespaceColor = theme.getColor(SYMBOL_ICON_NAMESPACE_FOREGROUND);
|
||||
if (outlineNamespaceColor) {
|
||||
collector.addRule(`${Codicon.symbolNamespace.cssSelector} { color: ${outlineNamespaceColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconNullColor = theme.getColor(SYMBOL_ICON_NULL_FOREGROUND);
|
||||
if (symbolIconNullColor) {
|
||||
collector.addRule(`${Codicon.symbolNull.cssSelector} { color: ${symbolIconNullColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconNumberColor = theme.getColor(SYMBOL_ICON_NUMBER_FOREGROUND);
|
||||
if (symbolIconNumberColor) {
|
||||
collector.addRule(`${Codicon.symbolNumber.cssSelector} { color: ${symbolIconNumberColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconObjectColor = theme.getColor(SYMBOL_ICON_OBJECT_FOREGROUND);
|
||||
if (symbolIconObjectColor) {
|
||||
collector.addRule(`${Codicon.symbolObject.cssSelector} { color: ${symbolIconObjectColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconOperatorColor = theme.getColor(SYMBOL_ICON_OPERATOR_FOREGROUND);
|
||||
if (symbolIconOperatorColor) {
|
||||
collector.addRule(`${Codicon.symbolOperator.cssSelector} { color: ${symbolIconOperatorColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconPackageColor = theme.getColor(SYMBOL_ICON_PACKAGE_FOREGROUND);
|
||||
if (symbolIconPackageColor) {
|
||||
collector.addRule(`${Codicon.symbolPackage.cssSelector} { color: ${symbolIconPackageColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconPropertyColor = theme.getColor(SYMBOL_ICON_PROPERTY_FOREGROUND);
|
||||
if (symbolIconPropertyColor) {
|
||||
collector.addRule(`${Codicon.symbolProperty.cssSelector} { color: ${symbolIconPropertyColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconReferenceColor = theme.getColor(SYMBOL_ICON_REFERENCE_FOREGROUND);
|
||||
if (symbolIconReferenceColor) {
|
||||
collector.addRule(`${Codicon.symbolReference.cssSelector} { color: ${symbolIconReferenceColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconSnippetColor = theme.getColor(SYMBOL_ICON_SNIPPET_FOREGROUND);
|
||||
if (symbolIconSnippetColor) {
|
||||
collector.addRule(`${Codicon.symbolSnippet.cssSelector} { color: ${symbolIconSnippetColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconStringColor = theme.getColor(SYMBOL_ICON_STRING_FOREGROUND);
|
||||
if (symbolIconStringColor) {
|
||||
collector.addRule(`${Codicon.symbolString.cssSelector} { color: ${symbolIconStringColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconStructColor = theme.getColor(SYMBOL_ICON_STRUCT_FOREGROUND);
|
||||
if (symbolIconStructColor) {
|
||||
collector.addRule(`${Codicon.symbolStruct.cssSelector} { color: ${symbolIconStructColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconTextColor = theme.getColor(SYMBOL_ICON_TEXT_FOREGROUND);
|
||||
if (symbolIconTextColor) {
|
||||
collector.addRule(`${Codicon.symbolText.cssSelector} { color: ${symbolIconTextColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconTypeParameterColor = theme.getColor(SYMBOL_ICON_TYPEPARAMETER_FOREGROUND);
|
||||
if (symbolIconTypeParameterColor) {
|
||||
collector.addRule(`${Codicon.symbolTypeParameter.cssSelector} { color: ${symbolIconTypeParameterColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconUnitColor = theme.getColor(SYMBOL_ICON_UNIT_FOREGROUND);
|
||||
if (symbolIconUnitColor) {
|
||||
collector.addRule(`${Codicon.symbolUnit.cssSelector} { color: ${symbolIconUnitColor}; }`);
|
||||
}
|
||||
|
||||
const symbolIconVariableColor = theme.getColor(SYMBOL_ICON_VARIABLE_FOREGROUND);
|
||||
if (symbolIconVariableColor) {
|
||||
collector.addRule(`${Codicon.symbolVariable.cssSelector} { color: ${symbolIconVariableColor}; }`);
|
||||
}
|
||||
|
||||
});
|
||||
@@ -0,0 +1,187 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { OutlineElement, OutlineGroup, OutlineModel } from '../outlineModel';
|
||||
import { SymbolKind, DocumentSymbol, DocumentSymbolProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IMarker, MarkerSeverity } from 'vs/platform/markers/common/markers';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
|
||||
suite('OutlineModel', function () {
|
||||
|
||||
test('OutlineModel#create, cached', async function () {
|
||||
|
||||
let model = createTextModel('foo', undefined, undefined, URI.file('/fome/path.foo'));
|
||||
let count = 0;
|
||||
let reg = DocumentSymbolProviderRegistry.register({ pattern: '**/path.foo' }, {
|
||||
provideDocumentSymbols() {
|
||||
count += 1;
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
await OutlineModel.create(model, CancellationToken.None);
|
||||
assert.equal(count, 1);
|
||||
|
||||
// cached
|
||||
await OutlineModel.create(model, CancellationToken.None);
|
||||
assert.equal(count, 1);
|
||||
|
||||
// new version
|
||||
model.applyEdits([{ text: 'XXX', range: new Range(1, 1, 1, 1) }]);
|
||||
await OutlineModel.create(model, CancellationToken.None);
|
||||
assert.equal(count, 2);
|
||||
|
||||
reg.dispose();
|
||||
});
|
||||
|
||||
test('OutlineModel#create, cached/cancel', async function () {
|
||||
|
||||
let model = createTextModel('foo', undefined, undefined, URI.file('/fome/path.foo'));
|
||||
let isCancelled = false;
|
||||
|
||||
let reg = DocumentSymbolProviderRegistry.register({ pattern: '**/path.foo' }, {
|
||||
provideDocumentSymbols(d, token) {
|
||||
return new Promise(resolve => {
|
||||
token.onCancellationRequested(_ => {
|
||||
isCancelled = true;
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(isCancelled, false);
|
||||
let s1 = new CancellationTokenSource();
|
||||
OutlineModel.create(model, s1.token);
|
||||
let s2 = new CancellationTokenSource();
|
||||
OutlineModel.create(model, s2.token);
|
||||
|
||||
s1.cancel();
|
||||
assert.equal(isCancelled, false);
|
||||
|
||||
s2.cancel();
|
||||
assert.equal(isCancelled, true);
|
||||
|
||||
reg.dispose();
|
||||
});
|
||||
|
||||
function fakeSymbolInformation(range: Range, name: string = 'foo'): DocumentSymbol {
|
||||
return {
|
||||
name,
|
||||
detail: 'fake',
|
||||
kind: SymbolKind.Boolean,
|
||||
tags: [],
|
||||
selectionRange: range,
|
||||
range: range
|
||||
};
|
||||
}
|
||||
|
||||
function fakeMarker(range: Range): IMarker {
|
||||
return { ...range, owner: 'ffff', message: 'test', severity: MarkerSeverity.Error, resource: null! };
|
||||
}
|
||||
|
||||
test('OutlineElement - updateMarker', function () {
|
||||
|
||||
let e0 = new OutlineElement('foo1', null!, fakeSymbolInformation(new Range(1, 1, 1, 10)));
|
||||
let e1 = new OutlineElement('foo2', null!, fakeSymbolInformation(new Range(2, 1, 5, 1)));
|
||||
let e2 = new OutlineElement('foo3', null!, fakeSymbolInformation(new Range(6, 1, 10, 10)));
|
||||
|
||||
let group = new OutlineGroup('group', null!, null!, 1);
|
||||
group.children.set(e0.id, e0);
|
||||
group.children.set(e1.id, e1);
|
||||
group.children.set(e2.id, e2);
|
||||
|
||||
const data = [fakeMarker(new Range(6, 1, 6, 7)), fakeMarker(new Range(1, 1, 1, 4)), fakeMarker(new Range(10, 2, 14, 1))];
|
||||
data.sort(Range.compareRangesUsingStarts); // model does this
|
||||
|
||||
group.updateMarker(data);
|
||||
assert.equal(data.length, 0); // all 'stolen'
|
||||
assert.equal(e0.marker!.count, 1);
|
||||
assert.equal(e1.marker, undefined);
|
||||
assert.equal(e2.marker!.count, 2);
|
||||
|
||||
group.updateMarker([]);
|
||||
assert.equal(e0.marker, undefined);
|
||||
assert.equal(e1.marker, undefined);
|
||||
assert.equal(e2.marker, undefined);
|
||||
});
|
||||
|
||||
test('OutlineElement - updateMarker, 2', function () {
|
||||
|
||||
let p = new OutlineElement('A', null!, fakeSymbolInformation(new Range(1, 1, 11, 1)));
|
||||
let c1 = new OutlineElement('A/B', null!, fakeSymbolInformation(new Range(2, 4, 5, 4)));
|
||||
let c2 = new OutlineElement('A/C', null!, fakeSymbolInformation(new Range(6, 4, 9, 4)));
|
||||
|
||||
let group = new OutlineGroup('group', null!, null!, 1);
|
||||
group.children.set(p.id, p);
|
||||
p.children.set(c1.id, c1);
|
||||
p.children.set(c2.id, c2);
|
||||
|
||||
let data = [
|
||||
fakeMarker(new Range(2, 4, 5, 4))
|
||||
];
|
||||
|
||||
group.updateMarker(data);
|
||||
assert.equal(p.marker!.count, 0);
|
||||
assert.equal(c1.marker!.count, 1);
|
||||
assert.equal(c2.marker, undefined);
|
||||
|
||||
data = [
|
||||
fakeMarker(new Range(2, 4, 5, 4)),
|
||||
fakeMarker(new Range(2, 6, 2, 8)),
|
||||
fakeMarker(new Range(7, 6, 7, 8)),
|
||||
];
|
||||
group.updateMarker(data);
|
||||
assert.equal(p.marker!.count, 0);
|
||||
assert.equal(c1.marker!.count, 2);
|
||||
assert.equal(c2.marker!.count, 1);
|
||||
|
||||
data = [
|
||||
fakeMarker(new Range(1, 4, 1, 11)),
|
||||
fakeMarker(new Range(7, 6, 7, 8)),
|
||||
];
|
||||
group.updateMarker(data);
|
||||
assert.equal(p.marker!.count, 1);
|
||||
assert.equal(c1.marker, undefined);
|
||||
assert.equal(c2.marker!.count, 1);
|
||||
});
|
||||
|
||||
test('OutlineElement - updateMarker/multiple groups', function () {
|
||||
|
||||
let model = new class extends OutlineModel {
|
||||
constructor() {
|
||||
super(null!);
|
||||
}
|
||||
readyForTesting() {
|
||||
this._groups = this.children as any;
|
||||
}
|
||||
};
|
||||
model.children.set('g1', new OutlineGroup('g1', model, null!, 1));
|
||||
model.children.get('g1')!.children.set('c1', new OutlineElement('c1', model.children.get('g1')!, fakeSymbolInformation(new Range(1, 1, 11, 1))));
|
||||
|
||||
model.children.set('g2', new OutlineGroup('g2', model, null!, 1));
|
||||
model.children.get('g2')!.children.set('c2', new OutlineElement('c2', model.children.get('g2')!, fakeSymbolInformation(new Range(1, 1, 7, 1))));
|
||||
model.children.get('g2')!.children.get('c2')!.children.set('c2.1', new OutlineElement('c2.1', model.children.get('g2')!.children.get('c2')!, fakeSymbolInformation(new Range(1, 3, 2, 19))));
|
||||
model.children.get('g2')!.children.get('c2')!.children.set('c2.2', new OutlineElement('c2.2', model.children.get('g2')!.children.get('c2')!, fakeSymbolInformation(new Range(4, 1, 6, 10))));
|
||||
|
||||
model.readyForTesting();
|
||||
|
||||
const data = [
|
||||
fakeMarker(new Range(1, 1, 2, 8)),
|
||||
fakeMarker(new Range(6, 1, 6, 98)),
|
||||
];
|
||||
|
||||
model.updateMarker(data);
|
||||
|
||||
assert.equal(model.children.get('g1')!.children.get('c1')!.marker!.count, 2);
|
||||
assert.equal(model.children.get('g2')!.children.get('c2')!.children.get('c2.1')!.marker!.count, 1);
|
||||
assert.equal(model.children.get('g2')!.children.get('c2')!.children.get('c2.2')!.marker!.count, 1);
|
||||
});
|
||||
|
||||
});
|
||||
933
lib/vscode/src/vs/editor/contrib/find/findController.ts
Normal file
933
lib/vscode/src/vs/editor/contrib/find/findController.ts
Normal file
@@ -0,0 +1,933 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution, MultiEditorAction, registerMultiEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, TogglePreserveCaseKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding, CONTEXT_REPLACE_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel';
|
||||
import { FindOptionsWidget } from 'vs/editor/contrib/find/findOptionsWidget';
|
||||
import { FindReplaceState, FindReplaceStateChangedEvent, INewFindReplaceState } from 'vs/editor/contrib/find/findState';
|
||||
import { FindWidget, IFindController } from 'vs/editor/contrib/find/findWidget';
|
||||
import { MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
|
||||
|
||||
const SEARCH_STRING_MAX_LENGTH = 524288;
|
||||
|
||||
export function getSelectionSearchString(editor: ICodeEditor): string | null {
|
||||
if (!editor.hasModel()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selection = editor.getSelection();
|
||||
// if selection spans multiple lines, default search string to empty
|
||||
if (selection.startLineNumber === selection.endLineNumber) {
|
||||
if (selection.isEmpty()) {
|
||||
const wordAtPosition = editor.getConfiguredWordAtPosition(selection.getStartPosition());
|
||||
if (wordAtPosition) {
|
||||
return wordAtPosition.word;
|
||||
}
|
||||
} else {
|
||||
if (editor.getModel().getValueLengthInRange(selection) < SEARCH_STRING_MAX_LENGTH) {
|
||||
return editor.getModel().getValueInRange(selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export const enum FindStartFocusAction {
|
||||
NoFocusChange,
|
||||
FocusFindInput,
|
||||
FocusReplaceInput
|
||||
}
|
||||
|
||||
export interface IFindStartOptions {
|
||||
forceRevealReplace: boolean;
|
||||
seedSearchStringFromSelection: boolean;
|
||||
seedSearchStringFromGlobalClipboard: boolean;
|
||||
shouldFocus: FindStartFocusAction;
|
||||
shouldAnimate: boolean;
|
||||
updateSearchScope: boolean;
|
||||
loop: boolean;
|
||||
}
|
||||
|
||||
export class CommonFindController extends Disposable implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.findController';
|
||||
|
||||
protected _editor: ICodeEditor;
|
||||
private readonly _findWidgetVisible: IContextKey<boolean>;
|
||||
protected _state: FindReplaceState;
|
||||
protected _updateHistoryDelayer: Delayer<void>;
|
||||
private _model: FindModelBoundToEditorModel | null;
|
||||
protected readonly _storageService: IStorageService;
|
||||
private readonly _clipboardService: IClipboardService;
|
||||
protected readonly _contextKeyService: IContextKeyService;
|
||||
|
||||
public static get(editor: ICodeEditor): CommonFindController {
|
||||
return editor.getContribution<CommonFindController>(CommonFindController.ID);
|
||||
}
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IClipboardService clipboardService: IClipboardService
|
||||
) {
|
||||
super();
|
||||
this._editor = editor;
|
||||
this._findWidgetVisible = CONTEXT_FIND_WIDGET_VISIBLE.bindTo(contextKeyService);
|
||||
this._contextKeyService = contextKeyService;
|
||||
this._storageService = storageService;
|
||||
this._clipboardService = clipboardService;
|
||||
|
||||
this._updateHistoryDelayer = new Delayer<void>(500);
|
||||
this._state = this._register(new FindReplaceState());
|
||||
this.loadQueryState();
|
||||
this._register(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e)));
|
||||
|
||||
this._model = null;
|
||||
|
||||
this._register(this._editor.onDidChangeModel(() => {
|
||||
let shouldRestartFind = (this._editor.getModel() && this._state.isRevealed);
|
||||
|
||||
this.disposeModel();
|
||||
|
||||
this._state.change({
|
||||
searchScope: null,
|
||||
matchCase: this._storageService.getBoolean('editor.matchCase', StorageScope.WORKSPACE, false),
|
||||
wholeWord: this._storageService.getBoolean('editor.wholeWord', StorageScope.WORKSPACE, false),
|
||||
isRegex: this._storageService.getBoolean('editor.isRegex', StorageScope.WORKSPACE, false),
|
||||
preserveCase: this._storageService.getBoolean('editor.preserveCase', StorageScope.WORKSPACE, false)
|
||||
}, false);
|
||||
|
||||
if (shouldRestartFind) {
|
||||
this._start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: false && this._editor.getOption(EditorOption.find).seedSearchStringFromSelection,
|
||||
seedSearchStringFromGlobalClipboard: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: false,
|
||||
updateSearchScope: false,
|
||||
loop: this._editor.getOption(EditorOption.find).loop
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.disposeModel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private disposeModel(): void {
|
||||
if (this._model) {
|
||||
this._model.dispose();
|
||||
this._model = null;
|
||||
}
|
||||
}
|
||||
|
||||
private _onStateChanged(e: FindReplaceStateChangedEvent): void {
|
||||
this.saveQueryState(e);
|
||||
|
||||
if (e.isRevealed) {
|
||||
if (this._state.isRevealed) {
|
||||
this._findWidgetVisible.set(true);
|
||||
} else {
|
||||
this._findWidgetVisible.reset();
|
||||
this.disposeModel();
|
||||
}
|
||||
}
|
||||
if (e.searchString) {
|
||||
this.setGlobalBufferTerm(this._state.searchString);
|
||||
}
|
||||
}
|
||||
|
||||
private saveQueryState(e: FindReplaceStateChangedEvent) {
|
||||
if (e.isRegex) {
|
||||
this._storageService.store('editor.isRegex', this._state.actualIsRegex, StorageScope.WORKSPACE);
|
||||
}
|
||||
if (e.wholeWord) {
|
||||
this._storageService.store('editor.wholeWord', this._state.actualWholeWord, StorageScope.WORKSPACE);
|
||||
}
|
||||
if (e.matchCase) {
|
||||
this._storageService.store('editor.matchCase', this._state.actualMatchCase, StorageScope.WORKSPACE);
|
||||
}
|
||||
if (e.preserveCase) {
|
||||
this._storageService.store('editor.preserveCase', this._state.actualPreserveCase, StorageScope.WORKSPACE);
|
||||
}
|
||||
}
|
||||
|
||||
private loadQueryState() {
|
||||
this._state.change({
|
||||
matchCase: this._storageService.getBoolean('editor.matchCase', StorageScope.WORKSPACE, this._state.matchCase),
|
||||
wholeWord: this._storageService.getBoolean('editor.wholeWord', StorageScope.WORKSPACE, this._state.wholeWord),
|
||||
isRegex: this._storageService.getBoolean('editor.isRegex', StorageScope.WORKSPACE, this._state.isRegex),
|
||||
preserveCase: this._storageService.getBoolean('editor.preserveCase', StorageScope.WORKSPACE, this._state.preserveCase)
|
||||
}, false);
|
||||
}
|
||||
|
||||
public isFindInputFocused(): boolean {
|
||||
return !!CONTEXT_FIND_INPUT_FOCUSED.getValue(this._contextKeyService);
|
||||
}
|
||||
|
||||
public getState(): FindReplaceState {
|
||||
return this._state;
|
||||
}
|
||||
|
||||
public closeFindWidget(): void {
|
||||
this._state.change({
|
||||
isRevealed: false,
|
||||
searchScope: null
|
||||
}, false);
|
||||
this._editor.focus();
|
||||
}
|
||||
|
||||
public toggleCaseSensitive(): void {
|
||||
this._state.change({ matchCase: !this._state.matchCase }, false);
|
||||
if (!this._state.isRevealed) {
|
||||
this.highlightFindOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public toggleWholeWords(): void {
|
||||
this._state.change({ wholeWord: !this._state.wholeWord }, false);
|
||||
if (!this._state.isRevealed) {
|
||||
this.highlightFindOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public toggleRegex(): void {
|
||||
this._state.change({ isRegex: !this._state.isRegex }, false);
|
||||
if (!this._state.isRevealed) {
|
||||
this.highlightFindOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public togglePreserveCase(): void {
|
||||
this._state.change({ preserveCase: !this._state.preserveCase }, false);
|
||||
if (!this._state.isRevealed) {
|
||||
this.highlightFindOptions();
|
||||
}
|
||||
}
|
||||
|
||||
public toggleSearchScope(): void {
|
||||
if (this._state.searchScope) {
|
||||
this._state.change({ searchScope: null }, true);
|
||||
} else {
|
||||
if (this._editor.hasModel()) {
|
||||
let selections = this._editor.getSelections();
|
||||
selections.map(selection => {
|
||||
if (selection.endColumn === 1 && selection.endLineNumber > selection.startLineNumber) {
|
||||
selection = selection.setEndPosition(
|
||||
selection.endLineNumber - 1,
|
||||
this._editor.getModel()!.getLineMaxColumn(selection.endLineNumber - 1)
|
||||
);
|
||||
}
|
||||
if (!selection.isEmpty()) {
|
||||
return selection;
|
||||
}
|
||||
return null;
|
||||
}).filter(element => !!element);
|
||||
|
||||
if (selections.length) {
|
||||
this._state.change({ searchScope: selections }, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public setSearchString(searchString: string): void {
|
||||
if (this._state.isRegex) {
|
||||
searchString = strings.escapeRegExpCharacters(searchString);
|
||||
}
|
||||
this._state.change({ searchString: searchString }, false);
|
||||
}
|
||||
|
||||
public highlightFindOptions(): void {
|
||||
// overwritten in subclass
|
||||
}
|
||||
|
||||
protected async _start(opts: IFindStartOptions): Promise<void> {
|
||||
this.disposeModel();
|
||||
|
||||
if (!this._editor.hasModel()) {
|
||||
// cannot do anything with an editor that doesn't have a model...
|
||||
return;
|
||||
}
|
||||
|
||||
let stateChanges: INewFindReplaceState = {
|
||||
isRevealed: true
|
||||
};
|
||||
|
||||
if (opts.seedSearchStringFromSelection) {
|
||||
let selectionSearchString = getSelectionSearchString(this._editor);
|
||||
if (selectionSearchString) {
|
||||
if (this._state.isRegex) {
|
||||
stateChanges.searchString = strings.escapeRegExpCharacters(selectionSearchString);
|
||||
} else {
|
||||
stateChanges.searchString = selectionSearchString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!stateChanges.searchString && opts.seedSearchStringFromGlobalClipboard) {
|
||||
let selectionSearchString = await this.getGlobalBufferTerm();
|
||||
|
||||
if (!this._editor.hasModel()) {
|
||||
// the editor has lost its model in the meantime
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectionSearchString) {
|
||||
stateChanges.searchString = selectionSearchString;
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite isReplaceRevealed
|
||||
if (opts.forceRevealReplace) {
|
||||
stateChanges.isReplaceRevealed = true;
|
||||
} else if (!this._findWidgetVisible.get()) {
|
||||
stateChanges.isReplaceRevealed = false;
|
||||
}
|
||||
|
||||
if (opts.updateSearchScope) {
|
||||
let currentSelections = this._editor.getSelections();
|
||||
if (currentSelections.some(selection => !selection.isEmpty())) {
|
||||
stateChanges.searchScope = currentSelections;
|
||||
}
|
||||
}
|
||||
|
||||
stateChanges.loop = opts.loop;
|
||||
|
||||
this._state.change(stateChanges, false);
|
||||
|
||||
if (!this._model) {
|
||||
this._model = new FindModelBoundToEditorModel(this._editor, this._state);
|
||||
}
|
||||
}
|
||||
|
||||
public start(opts: IFindStartOptions): Promise<void> {
|
||||
return this._start(opts);
|
||||
}
|
||||
|
||||
public moveToNextMatch(): boolean {
|
||||
if (this._model) {
|
||||
this._model.moveToNextMatch();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public moveToPrevMatch(): boolean {
|
||||
if (this._model) {
|
||||
this._model.moveToPrevMatch();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public replace(): boolean {
|
||||
if (this._model) {
|
||||
this._model.replace();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public replaceAll(): boolean {
|
||||
if (this._model) {
|
||||
this._model.replaceAll();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public selectAllMatches(): boolean {
|
||||
if (this._model) {
|
||||
this._model.selectAllMatches();
|
||||
this._editor.focus();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public async getGlobalBufferTerm(): Promise<string> {
|
||||
if (this._editor.getOption(EditorOption.find).globalFindClipboard
|
||||
&& this._editor.hasModel()
|
||||
&& !this._editor.getModel().isTooLargeForSyncing()
|
||||
) {
|
||||
return this._clipboardService.readFindText();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public setGlobalBufferTerm(text: string): void {
|
||||
if (this._editor.getOption(EditorOption.find).globalFindClipboard
|
||||
&& this._editor.hasModel()
|
||||
&& !this._editor.getModel().isTooLargeForSyncing()
|
||||
) {
|
||||
// intentionally not awaited
|
||||
this._clipboardService.writeFindText(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FindController extends CommonFindController implements IFindController {
|
||||
|
||||
private _widget: FindWidget | null;
|
||||
private _findOptionsWidget: FindOptionsWidget | null;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextViewService private readonly _contextViewService: IContextViewService,
|
||||
@IContextKeyService _contextKeyService: IContextKeyService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IThemeService private readonly _themeService: IThemeService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IStorageService _storageService: IStorageService,
|
||||
@IStorageKeysSyncRegistryService private readonly _storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
|
||||
@IClipboardService clipboardService: IClipboardService,
|
||||
) {
|
||||
super(editor, _contextKeyService, _storageService, clipboardService);
|
||||
this._widget = null;
|
||||
this._findOptionsWidget = null;
|
||||
}
|
||||
|
||||
protected async _start(opts: IFindStartOptions): Promise<void> {
|
||||
if (!this._widget) {
|
||||
this._createFindWidget();
|
||||
}
|
||||
|
||||
const selection = this._editor.getSelection();
|
||||
let updateSearchScope = false;
|
||||
|
||||
switch (this._editor.getOption(EditorOption.find).autoFindInSelection) {
|
||||
case 'always':
|
||||
updateSearchScope = true;
|
||||
break;
|
||||
case 'never':
|
||||
updateSearchScope = false;
|
||||
break;
|
||||
case 'multiline':
|
||||
const isSelectionMultipleLine = !!selection && selection.startLineNumber !== selection.endLineNumber;
|
||||
updateSearchScope = isSelectionMultipleLine;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
opts.updateSearchScope = updateSearchScope;
|
||||
|
||||
await super._start(opts);
|
||||
|
||||
if (this._widget) {
|
||||
if (opts.shouldFocus === FindStartFocusAction.FocusReplaceInput) {
|
||||
this._widget.focusReplaceInput();
|
||||
} else if (opts.shouldFocus === FindStartFocusAction.FocusFindInput) {
|
||||
this._widget.focusFindInput();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public highlightFindOptions(): void {
|
||||
if (!this._widget) {
|
||||
this._createFindWidget();
|
||||
}
|
||||
if (this._state.isRevealed) {
|
||||
this._widget!.highlightFindOptions();
|
||||
} else {
|
||||
this._findOptionsWidget!.highlightFindOptions();
|
||||
}
|
||||
}
|
||||
|
||||
private _createFindWidget() {
|
||||
this._widget = this._register(new FindWidget(this._editor, this, this._state, this._contextViewService, this._keybindingService, this._contextKeyService, this._themeService, this._storageService, this._notificationService, this._storageKeysSyncRegistryService));
|
||||
this._findOptionsWidget = this._register(new FindOptionsWidget(this._editor, this._state, this._keybindingService, this._themeService));
|
||||
}
|
||||
}
|
||||
|
||||
export class StartFindAction extends MultiEditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FIND_IDS.StartFindAction,
|
||||
label: nls.localize('startFindAction', "Find"),
|
||||
alias: 'Find',
|
||||
precondition: ContextKeyExpr.or(ContextKeyExpr.has('editorFocus'), ContextKeyExpr.has('editorIsOpen')),
|
||||
kbOpts: {
|
||||
kbExpr: null,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_F,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarEditMenu,
|
||||
group: '3_find',
|
||||
title: nls.localize({ key: 'miFind', comment: ['&& denotes a mnemonic'] }, "&&Find"),
|
||||
order: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async run(accessor: ServicesAccessor | null, editor: ICodeEditor): Promise<void> {
|
||||
let controller = CommonFindController.get(editor);
|
||||
if (controller) {
|
||||
await controller.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: editor.getOption(EditorOption.find).seedSearchStringFromSelection,
|
||||
seedSearchStringFromGlobalClipboard: editor.getOption(EditorOption.find).globalFindClipboard,
|
||||
shouldFocus: FindStartFocusAction.FocusFindInput,
|
||||
shouldAnimate: true,
|
||||
updateSearchScope: false,
|
||||
loop: editor.getOption(EditorOption.find).loop
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class StartFindWithSelectionAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FIND_IDS.StartFindWithSelection,
|
||||
label: nls.localize('startFindWithSelectionAction', "Find With Selection"),
|
||||
alias: 'Find With Selection',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: null,
|
||||
primary: 0,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_E,
|
||||
},
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
let controller = CommonFindController.get(editor);
|
||||
if (controller) {
|
||||
await controller.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: true,
|
||||
seedSearchStringFromGlobalClipboard: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: true,
|
||||
updateSearchScope: false,
|
||||
loop: editor.getOption(EditorOption.find).loop
|
||||
});
|
||||
|
||||
controller.setGlobalBufferTerm(controller.getState().searchString);
|
||||
}
|
||||
}
|
||||
}
|
||||
export abstract class MatchFindAction extends EditorAction {
|
||||
public async run(accessor: ServicesAccessor | null, editor: ICodeEditor): Promise<void> {
|
||||
let controller = CommonFindController.get(editor);
|
||||
if (controller && !this._run(controller)) {
|
||||
await controller.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: (controller.getState().searchString.length === 0) && editor.getOption(EditorOption.find).seedSearchStringFromSelection,
|
||||
seedSearchStringFromGlobalClipboard: true,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: true,
|
||||
updateSearchScope: false,
|
||||
loop: editor.getOption(EditorOption.find).loop
|
||||
});
|
||||
this._run(controller);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract _run(controller: CommonFindController): boolean;
|
||||
}
|
||||
|
||||
export class NextMatchFindAction extends MatchFindAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FIND_IDS.NextMatchFindAction,
|
||||
label: nls.localize('findNextMatchAction', "Find Next"),
|
||||
alias: 'Find Next',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyCode.F3,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyCode.KEY_G, secondary: [KeyCode.F3] },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _run(controller: CommonFindController): boolean {
|
||||
return controller.moveToNextMatch();
|
||||
}
|
||||
}
|
||||
|
||||
export class NextMatchFindAction2 extends MatchFindAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FIND_IDS.NextMatchFindAction,
|
||||
label: nls.localize('findNextMatchAction', "Find Next"),
|
||||
alias: 'Find Next',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED),
|
||||
primary: KeyCode.Enter,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _run(controller: CommonFindController): boolean {
|
||||
return controller.moveToNextMatch();
|
||||
}
|
||||
}
|
||||
|
||||
export class PreviousMatchFindAction extends MatchFindAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FIND_IDS.PreviousMatchFindAction,
|
||||
label: nls.localize('findPreviousMatchAction', "Find Previous"),
|
||||
alias: 'Find Previous',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.Shift | KeyCode.F3,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G, secondary: [KeyMod.Shift | KeyCode.F3] },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _run(controller: CommonFindController): boolean {
|
||||
return controller.moveToPrevMatch();
|
||||
}
|
||||
}
|
||||
|
||||
export class PreviousMatchFindAction2 extends MatchFindAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FIND_IDS.PreviousMatchFindAction,
|
||||
label: nls.localize('findPreviousMatchAction', "Find Previous"),
|
||||
alias: 'Find Previous',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_FIND_INPUT_FOCUSED),
|
||||
primary: KeyMod.Shift | KeyCode.Enter,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _run(controller: CommonFindController): boolean {
|
||||
return controller.moveToPrevMatch();
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class SelectionMatchFindAction extends EditorAction {
|
||||
public async run(accessor: ServicesAccessor | null, editor: ICodeEditor): Promise<void> {
|
||||
let controller = CommonFindController.get(editor);
|
||||
if (!controller) {
|
||||
return;
|
||||
}
|
||||
let selectionSearchString = getSelectionSearchString(editor);
|
||||
if (selectionSearchString) {
|
||||
controller.setSearchString(selectionSearchString);
|
||||
}
|
||||
if (!this._run(controller)) {
|
||||
await controller.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: editor.getOption(EditorOption.find).seedSearchStringFromSelection,
|
||||
seedSearchStringFromGlobalClipboard: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: true,
|
||||
updateSearchScope: false,
|
||||
loop: editor.getOption(EditorOption.find).loop
|
||||
});
|
||||
this._run(controller);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract _run(controller: CommonFindController): boolean;
|
||||
}
|
||||
|
||||
export class NextSelectionMatchFindAction extends SelectionMatchFindAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FIND_IDS.NextSelectionMatchFindAction,
|
||||
label: nls.localize('nextSelectionMatchFindAction', "Find Next Selection"),
|
||||
alias: 'Find Next Selection',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.F3,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _run(controller: CommonFindController): boolean {
|
||||
return controller.moveToNextMatch();
|
||||
}
|
||||
}
|
||||
|
||||
export class PreviousSelectionMatchFindAction extends SelectionMatchFindAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FIND_IDS.PreviousSelectionMatchFindAction,
|
||||
label: nls.localize('previousSelectionMatchFindAction', "Find Previous Selection"),
|
||||
alias: 'Find Previous Selection',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F3,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected _run(controller: CommonFindController): boolean {
|
||||
return controller.moveToPrevMatch();
|
||||
}
|
||||
}
|
||||
|
||||
export class StartFindReplaceAction extends MultiEditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: FIND_IDS.StartFindReplaceAction,
|
||||
label: nls.localize('startReplace', "Replace"),
|
||||
alias: 'Replace',
|
||||
precondition: ContextKeyExpr.or(ContextKeyExpr.has('editorFocus'), ContextKeyExpr.has('editorIsOpen')),
|
||||
kbOpts: {
|
||||
kbExpr: null,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KEY_H,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_F },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarEditMenu,
|
||||
group: '3_find',
|
||||
title: nls.localize({ key: 'miReplace', comment: ['&& denotes a mnemonic'] }, "&&Replace"),
|
||||
order: 2
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async run(accessor: ServicesAccessor | null, editor: ICodeEditor): Promise<void> {
|
||||
if (!editor.hasModel() || editor.getOption(EditorOption.readOnly)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let controller = CommonFindController.get(editor);
|
||||
let currentSelection = editor.getSelection();
|
||||
let findInputFocused = controller.isFindInputFocused();
|
||||
// we only seed search string from selection when the current selection is single line and not empty,
|
||||
// + the find input is not focused
|
||||
let seedSearchStringFromSelection = !currentSelection.isEmpty()
|
||||
&& currentSelection.startLineNumber === currentSelection.endLineNumber && editor.getOption(EditorOption.find).seedSearchStringFromSelection
|
||||
&& !findInputFocused;
|
||||
/*
|
||||
* if the existing search string in find widget is empty and we don't seed search string from selection, it means the Find Input is still empty, so we should focus the Find Input instead of Replace Input.
|
||||
|
||||
* findInputFocused true -> seedSearchStringFromSelection false, FocusReplaceInput
|
||||
* findInputFocused false, seedSearchStringFromSelection true FocusReplaceInput
|
||||
* findInputFocused false seedSearchStringFromSelection false FocusFindInput
|
||||
*/
|
||||
let shouldFocus = (findInputFocused || seedSearchStringFromSelection) ?
|
||||
FindStartFocusAction.FocusReplaceInput : FindStartFocusAction.FocusFindInput;
|
||||
|
||||
|
||||
if (controller) {
|
||||
await controller.start({
|
||||
forceRevealReplace: true,
|
||||
seedSearchStringFromSelection: seedSearchStringFromSelection,
|
||||
seedSearchStringFromGlobalClipboard: editor.getOption(EditorOption.find).seedSearchStringFromSelection,
|
||||
shouldFocus: shouldFocus,
|
||||
shouldAnimate: true,
|
||||
updateSearchScope: false,
|
||||
loop: editor.getOption(EditorOption.find).loop
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(CommonFindController.ID, FindController);
|
||||
|
||||
export const EditorStartFindAction = new StartFindAction();
|
||||
registerMultiEditorAction(EditorStartFindAction);
|
||||
registerEditorAction(StartFindWithSelectionAction);
|
||||
registerEditorAction(NextMatchFindAction);
|
||||
registerEditorAction(NextMatchFindAction2);
|
||||
registerEditorAction(PreviousMatchFindAction);
|
||||
registerEditorAction(PreviousMatchFindAction2);
|
||||
registerEditorAction(NextSelectionMatchFindAction);
|
||||
registerEditorAction(PreviousSelectionMatchFindAction);
|
||||
export const EditorStartFindReplaceAction = new StartFindReplaceAction();
|
||||
registerMultiEditorAction(EditorStartFindReplaceAction);
|
||||
|
||||
const FindCommand = EditorCommand.bindToContribution<CommonFindController>(CommonFindController.get);
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.CloseFindWidgetCommand,
|
||||
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
|
||||
handler: x => x.closeFindWidget(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, ContextKeyExpr.not('isComposing')),
|
||||
primary: KeyCode.Escape,
|
||||
secondary: [KeyMod.Shift | KeyCode.Escape]
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.ToggleCaseSensitiveCommand,
|
||||
precondition: undefined,
|
||||
handler: x => x.toggleCaseSensitive(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: ToggleCaseSensitiveKeybinding.primary,
|
||||
mac: ToggleCaseSensitiveKeybinding.mac,
|
||||
win: ToggleCaseSensitiveKeybinding.win,
|
||||
linux: ToggleCaseSensitiveKeybinding.linux
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.ToggleWholeWordCommand,
|
||||
precondition: undefined,
|
||||
handler: x => x.toggleWholeWords(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: ToggleWholeWordKeybinding.primary,
|
||||
mac: ToggleWholeWordKeybinding.mac,
|
||||
win: ToggleWholeWordKeybinding.win,
|
||||
linux: ToggleWholeWordKeybinding.linux
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.ToggleRegexCommand,
|
||||
precondition: undefined,
|
||||
handler: x => x.toggleRegex(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: ToggleRegexKeybinding.primary,
|
||||
mac: ToggleRegexKeybinding.mac,
|
||||
win: ToggleRegexKeybinding.win,
|
||||
linux: ToggleRegexKeybinding.linux
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.ToggleSearchScopeCommand,
|
||||
precondition: undefined,
|
||||
handler: x => x.toggleSearchScope(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: ToggleSearchScopeKeybinding.primary,
|
||||
mac: ToggleSearchScopeKeybinding.mac,
|
||||
win: ToggleSearchScopeKeybinding.win,
|
||||
linux: ToggleSearchScopeKeybinding.linux
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.TogglePreserveCaseCommand,
|
||||
precondition: undefined,
|
||||
handler: x => x.togglePreserveCase(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: TogglePreserveCaseKeybinding.primary,
|
||||
mac: TogglePreserveCaseKeybinding.mac,
|
||||
win: TogglePreserveCaseKeybinding.win,
|
||||
linux: TogglePreserveCaseKeybinding.linux
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.ReplaceOneAction,
|
||||
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
|
||||
handler: x => x.replace(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_1
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.ReplaceOneAction,
|
||||
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
|
||||
handler: x => x.replace(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED),
|
||||
primary: KeyCode.Enter
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.ReplaceAllAction,
|
||||
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
|
||||
handler: x => x.replaceAll(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.ReplaceAllAction,
|
||||
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
|
||||
handler: x => x.replaceAll(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: ContextKeyExpr.and(EditorContextKeys.focus, CONTEXT_REPLACE_INPUT_FOCUSED),
|
||||
primary: undefined,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Enter,
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
registerEditorCommand(new FindCommand({
|
||||
id: FIND_IDS.SelectAllMatchesAction,
|
||||
precondition: CONTEXT_FIND_WIDGET_VISIBLE,
|
||||
handler: x => x.selectAllMatches(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 5,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.Alt | KeyCode.Enter
|
||||
}
|
||||
}));
|
||||
331
lib/vscode/src/vs/editor/contrib/find/findDecorations.ts
Normal file
331
lib/vscode/src/vs/editor/contrib/find/findDecorations.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { FindMatch, IModelDecorationsChangeAccessor, IModelDeltaDecoration, OverviewRulerLane, TrackedRangeStickiness, MinimapPosition } from 'vs/editor/common/model';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { overviewRulerFindMatchForeground, minimapFindMatch } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class FindDecorations implements IDisposable {
|
||||
|
||||
private readonly _editor: IActiveCodeEditor;
|
||||
private _decorations: string[];
|
||||
private _overviewRulerApproximateDecorations: string[];
|
||||
private _findScopeDecorationIds: string[];
|
||||
private _rangeHighlightDecorationId: string | null;
|
||||
private _highlightedDecorationId: string | null;
|
||||
private _startPosition: Position;
|
||||
|
||||
constructor(editor: IActiveCodeEditor) {
|
||||
this._editor = editor;
|
||||
this._decorations = [];
|
||||
this._overviewRulerApproximateDecorations = [];
|
||||
this._findScopeDecorationIds = [];
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
this._startPosition = this._editor.getPosition();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._editor.deltaDecorations(this._allDecorations(), []);
|
||||
|
||||
this._decorations = [];
|
||||
this._overviewRulerApproximateDecorations = [];
|
||||
this._findScopeDecorationIds = [];
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._decorations = [];
|
||||
this._overviewRulerApproximateDecorations = [];
|
||||
this._findScopeDecorationIds = [];
|
||||
this._rangeHighlightDecorationId = null;
|
||||
this._highlightedDecorationId = null;
|
||||
}
|
||||
|
||||
public getCount(): number {
|
||||
return this._decorations.length;
|
||||
}
|
||||
|
||||
/** @deprecated use getFindScopes to support multiple selections */
|
||||
public getFindScope(): Range | null {
|
||||
if (this._findScopeDecorationIds[0]) {
|
||||
return this._editor.getModel().getDecorationRange(this._findScopeDecorationIds[0]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getFindScopes(): Range[] | null {
|
||||
if (this._findScopeDecorationIds.length) {
|
||||
const scopes = this._findScopeDecorationIds.map(findScopeDecorationId =>
|
||||
this._editor.getModel().getDecorationRange(findScopeDecorationId)
|
||||
).filter(element => !!element);
|
||||
if (scopes.length) {
|
||||
return scopes as Range[];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public getStartPosition(): Position {
|
||||
return this._startPosition;
|
||||
}
|
||||
|
||||
public setStartPosition(newStartPosition: Position): void {
|
||||
this._startPosition = newStartPosition;
|
||||
this.setCurrentFindMatch(null);
|
||||
}
|
||||
|
||||
private _getDecorationIndex(decorationId: string): number {
|
||||
const index = this._decorations.indexOf(decorationId);
|
||||
if (index >= 0) {
|
||||
return index + 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
public getCurrentMatchesPosition(desiredRange: Range): number {
|
||||
let candidates = this._editor.getModel().getDecorationsInRange(desiredRange);
|
||||
for (const candidate of candidates) {
|
||||
const candidateOpts = candidate.options;
|
||||
if (candidateOpts === FindDecorations._FIND_MATCH_DECORATION || candidateOpts === FindDecorations._CURRENT_FIND_MATCH_DECORATION) {
|
||||
return this._getDecorationIndex(candidate.id);
|
||||
}
|
||||
}
|
||||
// We don't know the current match position, so returns zero to show '?' in find widget
|
||||
return 0;
|
||||
}
|
||||
|
||||
public setCurrentFindMatch(nextMatch: Range | null): number {
|
||||
let newCurrentDecorationId: string | null = 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: IModelDecorationsChangeAccessor) => {
|
||||
if (this._highlightedDecorationId !== null) {
|
||||
changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations._FIND_MATCH_DECORATION);
|
||||
this._highlightedDecorationId = null;
|
||||
}
|
||||
if (newCurrentDecorationId !== null) {
|
||||
this._highlightedDecorationId = newCurrentDecorationId;
|
||||
changeAccessor.changeDecorationOptions(this._highlightedDecorationId, FindDecorations._CURRENT_FIND_MATCH_DECORATION);
|
||||
}
|
||||
if (this._rangeHighlightDecorationId !== null) {
|
||||
changeAccessor.removeDecoration(this._rangeHighlightDecorationId);
|
||||
this._rangeHighlightDecorationId = null;
|
||||
}
|
||||
if (newCurrentDecorationId !== null) {
|
||||
let rng = this._editor.getModel().getDecorationRange(newCurrentDecorationId)!;
|
||||
if (rng.startLineNumber !== rng.endLineNumber && rng.endColumn === 1) {
|
||||
let lineBeforeEnd = rng.endLineNumber - 1;
|
||||
let lineBeforeEndMaxColumn = this._editor.getModel().getLineMaxColumn(lineBeforeEnd);
|
||||
rng = new Range(rng.startLineNumber, rng.startColumn, lineBeforeEnd, lineBeforeEndMaxColumn);
|
||||
}
|
||||
this._rangeHighlightDecorationId = changeAccessor.addDecoration(rng, FindDecorations._RANGE_HIGHLIGHT_DECORATION);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return matchPosition;
|
||||
}
|
||||
|
||||
public set(findMatches: FindMatch[], findScopes: Range[] | null): void {
|
||||
this._editor.changeDecorations((accessor) => {
|
||||
|
||||
let findMatchesOptions: ModelDecorationOptions = FindDecorations._FIND_MATCH_DECORATION;
|
||||
let newOverviewRulerApproximateDecorations: IModelDeltaDecoration[] = [];
|
||||
|
||||
if (findMatches.length > 1000) {
|
||||
// we go into a mode where the overview ruler gets "approximate" decorations
|
||||
// the reason is that the overview ruler paints all the decorations in the file and we don't want to cause freezes
|
||||
findMatchesOptions = FindDecorations._FIND_MATCH_NO_OVERVIEW_DECORATION;
|
||||
|
||||
// approximate a distance in lines where matches should be merged
|
||||
const lineCount = this._editor.getModel().getLineCount();
|
||||
const height = this._editor.getLayoutInfo().height;
|
||||
const approxPixelsPerLine = height / lineCount;
|
||||
const mergeLinesDelta = Math.max(2, Math.ceil(3 / approxPixelsPerLine));
|
||||
|
||||
// merge decorations as much as possible
|
||||
let prevStartLineNumber = findMatches[0].range.startLineNumber;
|
||||
let prevEndLineNumber = findMatches[0].range.endLineNumber;
|
||||
for (let i = 1, len = findMatches.length; i < len; i++) {
|
||||
const range = findMatches[i].range;
|
||||
if (prevEndLineNumber + mergeLinesDelta >= range.startLineNumber) {
|
||||
if (range.endLineNumber > prevEndLineNumber) {
|
||||
prevEndLineNumber = range.endLineNumber;
|
||||
}
|
||||
} else {
|
||||
newOverviewRulerApproximateDecorations.push({
|
||||
range: new Range(prevStartLineNumber, 1, prevEndLineNumber, 1),
|
||||
options: FindDecorations._FIND_MATCH_ONLY_OVERVIEW_DECORATION
|
||||
});
|
||||
prevStartLineNumber = range.startLineNumber;
|
||||
prevEndLineNumber = range.endLineNumber;
|
||||
}
|
||||
}
|
||||
|
||||
newOverviewRulerApproximateDecorations.push({
|
||||
range: new Range(prevStartLineNumber, 1, prevEndLineNumber, 1),
|
||||
options: FindDecorations._FIND_MATCH_ONLY_OVERVIEW_DECORATION
|
||||
});
|
||||
}
|
||||
|
||||
// Find matches
|
||||
let newFindMatchesDecorations: IModelDeltaDecoration[] = new Array<IModelDeltaDecoration>(findMatches.length);
|
||||
for (let i = 0, len = findMatches.length; i < len; i++) {
|
||||
newFindMatchesDecorations[i] = {
|
||||
range: findMatches[i].range,
|
||||
options: findMatchesOptions
|
||||
};
|
||||
}
|
||||
this._decorations = accessor.deltaDecorations(this._decorations, newFindMatchesDecorations);
|
||||
|
||||
// Overview ruler approximate decorations
|
||||
this._overviewRulerApproximateDecorations = accessor.deltaDecorations(this._overviewRulerApproximateDecorations, newOverviewRulerApproximateDecorations);
|
||||
|
||||
// Range highlight
|
||||
if (this._rangeHighlightDecorationId) {
|
||||
accessor.removeDecoration(this._rangeHighlightDecorationId);
|
||||
this._rangeHighlightDecorationId = null;
|
||||
}
|
||||
|
||||
// Find scope
|
||||
if (this._findScopeDecorationIds.length) {
|
||||
this._findScopeDecorationIds.forEach(findScopeDecorationId => accessor.removeDecoration(findScopeDecorationId));
|
||||
this._findScopeDecorationIds = [];
|
||||
}
|
||||
if (findScopes?.length) {
|
||||
this._findScopeDecorationIds = findScopes.map(findScope => accessor.addDecoration(findScope, FindDecorations._FIND_SCOPE_DECORATION));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public matchBeforePosition(position: Position): Range | null {
|
||||
if (this._decorations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
for (let i = this._decorations.length - 1; i >= 0; i--) {
|
||||
let decorationId = this._decorations[i];
|
||||
let r = this._editor.getModel().getDecorationRange(decorationId);
|
||||
if (!r || r.endLineNumber > position.lineNumber) {
|
||||
continue;
|
||||
}
|
||||
if (r.endLineNumber < position.lineNumber) {
|
||||
return r;
|
||||
}
|
||||
if (r.endColumn > position.column) {
|
||||
continue;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
return this._editor.getModel().getDecorationRange(this._decorations[this._decorations.length - 1]);
|
||||
}
|
||||
|
||||
public matchAfterPosition(position: Position): Range | null {
|
||||
if (this._decorations.length === 0) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0, len = this._decorations.length; i < len; i++) {
|
||||
let decorationId = this._decorations[i];
|
||||
let r = this._editor.getModel().getDecorationRange(decorationId);
|
||||
if (!r || r.startLineNumber < position.lineNumber) {
|
||||
continue;
|
||||
}
|
||||
if (r.startLineNumber > position.lineNumber) {
|
||||
return r;
|
||||
}
|
||||
if (r.startColumn < position.column) {
|
||||
continue;
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
return this._editor.getModel().getDecorationRange(this._decorations[0]);
|
||||
}
|
||||
|
||||
private _allDecorations(): string[] {
|
||||
let result: string[] = [];
|
||||
result = result.concat(this._decorations);
|
||||
result = result.concat(this._overviewRulerApproximateDecorations);
|
||||
if (this._findScopeDecorationIds.length) {
|
||||
result.push(...this._findScopeDecorationIds);
|
||||
}
|
||||
if (this._rangeHighlightDecorationId) {
|
||||
result.push(this._rangeHighlightDecorationId);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static readonly _CURRENT_FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
zIndex: 13,
|
||||
className: 'currentFindMatch',
|
||||
showIfCollapsed: true,
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(overviewRulerFindMatchForeground),
|
||||
position: OverviewRulerLane.Center
|
||||
},
|
||||
minimap: {
|
||||
color: themeColorFromId(minimapFindMatch),
|
||||
position: MinimapPosition.Inline
|
||||
}
|
||||
});
|
||||
|
||||
public static readonly _FIND_MATCH_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'findMatch',
|
||||
showIfCollapsed: true,
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(overviewRulerFindMatchForeground),
|
||||
position: OverviewRulerLane.Center
|
||||
},
|
||||
minimap: {
|
||||
color: themeColorFromId(minimapFindMatch),
|
||||
position: MinimapPosition.Inline
|
||||
}
|
||||
});
|
||||
|
||||
public static readonly _FIND_MATCH_NO_OVERVIEW_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'findMatch',
|
||||
showIfCollapsed: true
|
||||
});
|
||||
|
||||
private static readonly _FIND_MATCH_ONLY_OVERVIEW_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(overviewRulerFindMatchForeground),
|
||||
position: OverviewRulerLane.Center
|
||||
}
|
||||
});
|
||||
|
||||
private static readonly _RANGE_HIGHLIGHT_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'rangeHighlight',
|
||||
isWholeLine: true
|
||||
});
|
||||
|
||||
private static readonly _FIND_SCOPE_DECORATION = ModelDecorationOptions.register({
|
||||
className: 'findScope',
|
||||
isWholeLine: true
|
||||
});
|
||||
}
|
||||
596
lib/vscode/src/vs/editor/contrib/find/findModel.ts
Normal file
596
lib/vscode/src/vs/editor/contrib/find/findModel.ts
Normal file
@@ -0,0 +1,596 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { RunOnceScheduler, TimeoutTimer } from 'vs/base/common/async';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { dispose, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ReplaceCommand, ReplaceCommandThatPreservesSelection } from 'vs/editor/common/commands/replaceCommand';
|
||||
import { CursorChangeReason, ICursorPositionChangedEvent } from 'vs/editor/common/controller/cursorEvents';
|
||||
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 { Constants } from 'vs/base/common/uint';
|
||||
import { ScrollType, ICommand } from 'vs/editor/common/editorCommon';
|
||||
import { EndOfLinePreference, FindMatch, ITextModel } from 'vs/editor/common/model';
|
||||
import { SearchParams } from 'vs/editor/common/model/textModelSearch';
|
||||
import { FindDecorations } from 'vs/editor/contrib/find/findDecorations';
|
||||
import { FindReplaceState, FindReplaceStateChangedEvent } from 'vs/editor/contrib/find/findState';
|
||||
import { ReplaceAllCommand } from 'vs/editor/contrib/find/replaceAllCommand';
|
||||
import { ReplacePattern, parseReplaceString } from 'vs/editor/contrib/find/replacePattern';
|
||||
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IKeybindings } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { findFirstInSorted } from 'vs/base/common/arrays';
|
||||
|
||||
export const CONTEXT_FIND_WIDGET_VISIBLE = new RawContextKey<boolean>('findWidgetVisible', false);
|
||||
export const CONTEXT_FIND_WIDGET_NOT_VISIBLE = CONTEXT_FIND_WIDGET_VISIBLE.toNegated();
|
||||
// Keep ContextKey use of 'Focussed' to not break when clauses
|
||||
export const CONTEXT_FIND_INPUT_FOCUSED = new RawContextKey<boolean>('findInputFocussed', false);
|
||||
export const CONTEXT_REPLACE_INPUT_FOCUSED = new RawContextKey<boolean>('replaceInputFocussed', false);
|
||||
|
||||
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 TogglePreserveCaseKeybinding: IKeybindings = {
|
||||
primary: KeyMod.Alt | KeyCode.KEY_P,
|
||||
mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KEY_P }
|
||||
};
|
||||
|
||||
export const FIND_IDS = {
|
||||
StartFindAction: 'actions.find',
|
||||
StartFindWithSelection: 'actions.findWithSelection',
|
||||
NextMatchFindAction: 'editor.action.nextMatchFindAction',
|
||||
PreviousMatchFindAction: 'editor.action.previousMatchFindAction',
|
||||
NextSelectionMatchFindAction: 'editor.action.nextSelectionMatchFindAction',
|
||||
PreviousSelectionMatchFindAction: 'editor.action.previousSelectionMatchFindAction',
|
||||
StartFindReplaceAction: 'editor.action.startFindReplaceAction',
|
||||
CloseFindWidgetCommand: 'closeFindWidget',
|
||||
ToggleCaseSensitiveCommand: 'toggleFindCaseSensitive',
|
||||
ToggleWholeWordCommand: 'toggleFindWholeWord',
|
||||
ToggleRegexCommand: 'toggleFindRegex',
|
||||
ToggleSearchScopeCommand: 'toggleFindInSelection',
|
||||
TogglePreserveCaseCommand: 'togglePreserveCase',
|
||||
ReplaceOneAction: 'editor.action.replaceOne',
|
||||
ReplaceAllAction: 'editor.action.replaceAll',
|
||||
SelectAllMatchesAction: 'editor.action.selectAllMatches'
|
||||
};
|
||||
|
||||
export const MATCHES_LIMIT = 19999;
|
||||
const RESEARCH_DELAY = 240;
|
||||
|
||||
export class FindModelBoundToEditorModel {
|
||||
|
||||
private readonly _editor: IActiveCodeEditor;
|
||||
private readonly _state: FindReplaceState;
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
private readonly _decorations: FindDecorations;
|
||||
private _ignoreModelContentChanged: boolean;
|
||||
private readonly _startSearchingTimer: TimeoutTimer;
|
||||
|
||||
private readonly _updateDecorationsScheduler: RunOnceScheduler;
|
||||
private _isDisposed: boolean;
|
||||
|
||||
constructor(editor: IActiveCodeEditor, state: FindReplaceState) {
|
||||
this._editor = editor;
|
||||
this._state = state;
|
||||
this._isDisposed = false;
|
||||
this._startSearchingTimer = new TimeoutTimer();
|
||||
|
||||
this._decorations = new FindDecorations(editor);
|
||||
this._toDispose.add(this._decorations);
|
||||
|
||||
this._updateDecorationsScheduler = new RunOnceScheduler(() => this.research(false), 100);
|
||||
this._toDispose.add(this._updateDecorationsScheduler);
|
||||
|
||||
this._toDispose.add(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.add(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.add(this._state.onFindReplaceStateChange((e) => this._onStateChanged(e)));
|
||||
|
||||
this.research(false, this._state.searchScope);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
dispose(this._startSearchingTimer);
|
||||
this._toDispose.dispose();
|
||||
}
|
||||
|
||||
private _onStateChanged(e: FindReplaceStateChangedEvent): void {
|
||||
if (this._isDisposed) {
|
||||
// The find model is disposed during a find state changed event
|
||||
return;
|
||||
}
|
||||
if (!this._editor.hasModel()) {
|
||||
// The find model will be disposed momentarily
|
||||
return;
|
||||
}
|
||||
if (e.searchString || e.isReplaceRevealed || e.isRegex || e.wholeWord || e.matchCase || e.searchScope) {
|
||||
let model = this._editor.getModel();
|
||||
|
||||
if (model.isTooLargeForSyncing()) {
|
||||
this._startSearchingTimer.cancel();
|
||||
|
||||
this._startSearchingTimer.setIfNotSet(() => {
|
||||
if (e.searchScope) {
|
||||
this.research(e.moveCursor, this._state.searchScope);
|
||||
} else {
|
||||
this.research(e.moveCursor);
|
||||
}
|
||||
}, RESEARCH_DELAY);
|
||||
} else {
|
||||
if (e.searchScope) {
|
||||
this.research(e.moveCursor, this._state.searchScope);
|
||||
} else {
|
||||
this.research(e.moveCursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static _getSearchRange(model: ITextModel, findScope: Range | null): Range {
|
||||
// If we have set now or before a find scope, use it for computing the search range
|
||||
if (findScope) {
|
||||
return findScope;
|
||||
}
|
||||
|
||||
return model.getFullModelRange();
|
||||
}
|
||||
|
||||
private research(moveCursor: boolean, newFindScope?: Range | Range[] | null): void {
|
||||
let findScopes: Range[] | null = null;
|
||||
if (typeof newFindScope !== 'undefined') {
|
||||
if (newFindScope !== null) {
|
||||
if (!Array.isArray(newFindScope)) {
|
||||
findScopes = [newFindScope as Range];
|
||||
} else {
|
||||
findScopes = newFindScope;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
findScopes = this._decorations.getFindScopes();
|
||||
}
|
||||
if (findScopes !== null) {
|
||||
findScopes = findScopes.map(findScope => {
|
||||
if (findScope.startLineNumber !== findScope.endLineNumber) {
|
||||
let endLineNumber = findScope.endLineNumber;
|
||||
|
||||
if (findScope.endColumn === 1) {
|
||||
endLineNumber = endLineNumber - 1;
|
||||
}
|
||||
|
||||
return new Range(findScope.startLineNumber, 1, endLineNumber, this._editor.getModel().getLineMaxColumn(endLineNumber));
|
||||
}
|
||||
return findScope;
|
||||
});
|
||||
}
|
||||
|
||||
let findMatches = this._findMatches(findScopes, false, MATCHES_LIMIT);
|
||||
this._decorations.set(findMatches, findScopes);
|
||||
|
||||
const editorSelection = this._editor.getSelection();
|
||||
let currentMatchesPosition = this._decorations.getCurrentMatchesPosition(editorSelection);
|
||||
if (currentMatchesPosition === 0 && findMatches.length > 0) {
|
||||
// current selection is not on top of a match
|
||||
// try to find its nearest result from the top of the document
|
||||
const matchAfterSelection = findFirstInSorted(findMatches.map(match => match.range), range => Range.compareRangesUsingStarts(range, editorSelection) >= 0);
|
||||
currentMatchesPosition = matchAfterSelection > 0 ? matchAfterSelection - 1 + 1 /** match position is one based */ : currentMatchesPosition;
|
||||
}
|
||||
|
||||
this._state.changeMatchInfo(
|
||||
currentMatchesPosition,
|
||||
this._decorations.getCount(),
|
||||
undefined
|
||||
);
|
||||
|
||||
if (moveCursor && this._editor.getOption(EditorOption.find).cursorMoveOnType) {
|
||||
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, 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, ScrollType.Smooth);
|
||||
}
|
||||
|
||||
private _prevSearchPosition(before: Position) {
|
||||
let isUsingLineStops = this._state.isRegex && (
|
||||
this._state.searchString.indexOf('^') >= 0
|
||||
|| this._state.searchString.indexOf('$') >= 0
|
||||
);
|
||||
let { lineNumber, column } = before;
|
||||
let model = this._editor.getModel();
|
||||
|
||||
if (isUsingLineStops || column === 1) {
|
||||
if (lineNumber === 1) {
|
||||
lineNumber = model.getLineCount();
|
||||
} else {
|
||||
lineNumber--;
|
||||
}
|
||||
column = model.getLineMaxColumn(lineNumber);
|
||||
} else {
|
||||
column--;
|
||||
}
|
||||
|
||||
return new Position(lineNumber, column);
|
||||
}
|
||||
|
||||
private _moveToPrevMatch(before: Position, isRecursed: boolean = false): void {
|
||||
if (!this._state.canNavigateBack()) {
|
||||
// we are beyond the first matched find result
|
||||
// instead of doing nothing, we should refocus the first item
|
||||
const nextMatchRange = this._decorations.matchAfterPosition(before);
|
||||
|
||||
if (nextMatchRange) {
|
||||
this._setCurrentFindMatch(nextMatchRange);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this._decorations.getCount() < MATCHES_LIMIT) {
|
||||
let prevMatchRange = this._decorations.matchBeforePosition(before);
|
||||
|
||||
if (prevMatchRange && prevMatchRange.isEmpty() && prevMatchRange.getStartPosition().equals(before)) {
|
||||
before = this._prevSearchPosition(before);
|
||||
prevMatchRange = this._decorations.matchBeforePosition(before);
|
||||
}
|
||||
|
||||
if (prevMatchRange) {
|
||||
this._setCurrentFindMatch(prevMatchRange);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._cannotFind()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let findScope = this._decorations.getFindScope();
|
||||
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), 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.getOption(EditorOption.wordSeparators) : null, false);
|
||||
|
||||
if (prevMatch && prevMatch.range.isEmpty() && prevMatch.range.getStartPosition().equals(position)) {
|
||||
// Looks like we're stuck at this position, unacceptable!
|
||||
position = this._prevSearchPosition(position);
|
||||
prevMatch = model.findPreviousMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, false);
|
||||
}
|
||||
|
||||
if (!prevMatch) {
|
||||
// there is precisely one match and selection is on top of it
|
||||
return;
|
||||
}
|
||||
|
||||
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 _nextSearchPosition(after: Position) {
|
||||
let isUsingLineStops = this._state.isRegex && (
|
||||
this._state.searchString.indexOf('^') >= 0
|
||||
|| this._state.searchString.indexOf('$') >= 0
|
||||
);
|
||||
|
||||
let { lineNumber, column } = after;
|
||||
let model = this._editor.getModel();
|
||||
|
||||
if (isUsingLineStops || column === model.getLineMaxColumn(lineNumber)) {
|
||||
if (lineNumber === model.getLineCount()) {
|
||||
lineNumber = 1;
|
||||
} else {
|
||||
lineNumber++;
|
||||
}
|
||||
column = 1;
|
||||
} else {
|
||||
column++;
|
||||
}
|
||||
|
||||
return new Position(lineNumber, column);
|
||||
}
|
||||
|
||||
private _moveToNextMatch(after: Position): void {
|
||||
if (!this._state.canNavigateForward()) {
|
||||
// we are beyond the last matched find result
|
||||
// instead of doing nothing, we should refocus the last item
|
||||
const prevMatchRange = this._decorations.matchBeforePosition(after);
|
||||
|
||||
if (prevMatchRange) {
|
||||
this._setCurrentFindMatch(prevMatchRange);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this._decorations.getCount() < MATCHES_LIMIT) {
|
||||
let nextMatchRange = this._decorations.matchAfterPosition(after);
|
||||
|
||||
if (nextMatchRange && nextMatchRange.isEmpty() && nextMatchRange.getStartPosition().equals(after)) {
|
||||
// Looks like we're stuck at this position, unacceptable!
|
||||
after = this._nextSearchPosition(after);
|
||||
nextMatchRange = this._decorations.matchAfterPosition(after);
|
||||
}
|
||||
if (nextMatchRange) {
|
||||
this._setCurrentFindMatch(nextMatchRange);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let nextMatch = this._getNextMatch(after, false, true);
|
||||
if (nextMatch) {
|
||||
this._setCurrentFindMatch(nextMatch.range);
|
||||
}
|
||||
}
|
||||
|
||||
private _getNextMatch(after: Position, captureMatches: boolean, forceMove: boolean, isRecursed: boolean = false): FindMatch | null {
|
||||
if (this._cannotFind()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let findScope = this._decorations.getFindScope();
|
||||
let searchRange = FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), 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.getOption(EditorOption.wordSeparators) : null, captureMatches);
|
||||
|
||||
if (forceMove && nextMatch && nextMatch.range.isEmpty() && nextMatch.range.getStartPosition().equals(position)) {
|
||||
// Looks like we're stuck at this position, unacceptable!
|
||||
position = this._nextSearchPosition(position);
|
||||
nextMatch = model.findNextMatch(this._state.searchString, position, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.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(), true, false);
|
||||
if (nextMatch) {
|
||||
if (selection.equalsRange(nextMatch.range)) {
|
||||
// selection sits on a find match => replace it!
|
||||
let replaceString = replacePattern.buildReplaceString(nextMatch.matches, this._state.preserveCase);
|
||||
|
||||
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(findScopes: Range[] | null, captureMatches: boolean, limitResultCount: number): FindMatch[] {
|
||||
const searchRanges = (findScopes as [] || [null]).map((scope: Range | null) =>
|
||||
FindModelBoundToEditorModel._getSearchRange(this._editor.getModel(), scope)
|
||||
);
|
||||
|
||||
return this._editor.getModel().findMatches(this._state.searchString, searchRanges, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null, captureMatches, limitResultCount);
|
||||
}
|
||||
|
||||
public replaceAll(): void {
|
||||
if (!this._hasMatches()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const findScopes = this._decorations.getFindScopes();
|
||||
|
||||
if (findScopes === null && this._state.matchesCount >= MATCHES_LIMIT) {
|
||||
// Doing a replace on the entire file that is over ${MATCHES_LIMIT} matches
|
||||
this._largeReplaceAll();
|
||||
} else {
|
||||
this._regularReplaceAll(findScopes);
|
||||
}
|
||||
|
||||
this.research(false);
|
||||
}
|
||||
|
||||
private _largeReplaceAll(): void {
|
||||
const searchParams = new SearchParams(this._state.searchString, this._state.isRegex, this._state.matchCase, this._state.wholeWord ? this._editor.getOption(EditorOption.wordSeparators) : null);
|
||||
const searchData = searchParams.parseSearchRequest();
|
||||
if (!searchData) {
|
||||
return;
|
||||
}
|
||||
|
||||
let searchRegex = searchData.regex;
|
||||
if (!searchRegex.multiline) {
|
||||
let mod = 'mu';
|
||||
if (searchRegex.ignoreCase) {
|
||||
mod += 'i';
|
||||
}
|
||||
if (searchRegex.global) {
|
||||
mod += 'g';
|
||||
}
|
||||
searchRegex = new RegExp(searchRegex.source, mod);
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const modelText = model.getValue(EndOfLinePreference.LF);
|
||||
const fullModelRange = model.getFullModelRange();
|
||||
|
||||
const replacePattern = this._getReplacePattern();
|
||||
let resultText: string;
|
||||
const preserveCase = this._state.preserveCase;
|
||||
|
||||
if (replacePattern.hasReplacementPatterns || preserveCase) {
|
||||
resultText = modelText.replace(searchRegex, function () {
|
||||
return replacePattern.buildReplaceString(<string[]><any>arguments, preserveCase);
|
||||
});
|
||||
} else {
|
||||
resultText = modelText.replace(searchRegex, replacePattern.buildReplaceString(null, preserveCase));
|
||||
}
|
||||
|
||||
let command = new ReplaceCommandThatPreservesSelection(fullModelRange, resultText, this._editor.getSelection());
|
||||
this._executeEditorCommand('replaceAll', command);
|
||||
}
|
||||
|
||||
private _regularReplaceAll(findScopes: Range[] | null): void {
|
||||
const replacePattern = this._getReplacePattern();
|
||||
// Get all the ranges (even more than the highlighted ones)
|
||||
let matches = this._findMatches(findScopes, replacePattern.hasReplacementPatterns || this._state.preserveCase, 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, this._state.preserveCase);
|
||||
}
|
||||
|
||||
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 findScopes = this._decorations.getFindScopes();
|
||||
|
||||
// Get all the ranges (even more than the highlighted ones)
|
||||
let matches = this._findMatches(findScopes, 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: ICommand): void {
|
||||
try {
|
||||
this._ignoreModelContentChanged = true;
|
||||
this._editor.pushUndoStop();
|
||||
this._editor.executeCommand(source, command);
|
||||
this._editor.pushUndoStop();
|
||||
} finally {
|
||||
this._ignoreModelContentChanged = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
223
lib/vscode/src/vs/editor/contrib/find/findOptionsWidget.ts
Normal file
223
lib/vscode/src/vs/editor/contrib/find/findOptionsWidget.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { CaseSensitiveCheckbox, RegexCheckbox, WholeWordsCheckbox } from 'vs/base/browser/ui/findinput/findInputCheckboxes';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
|
||||
import { FIND_IDS } from 'vs/editor/contrib/find/findModel';
|
||||
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { contrastBorder, editorWidgetBackground, inputActiveOptionBorder, inputActiveOptionBackground, widgetShadow, editorWidgetForeground, inputActiveOptionForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
|
||||
export class FindOptionsWidget extends Widget implements IOverlayWidget {
|
||||
|
||||
private static readonly ID = 'editor.contrib.findOptionsWidget';
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
private readonly _state: FindReplaceState;
|
||||
private readonly _keybindingService: IKeybindingService;
|
||||
|
||||
private readonly _domNode: HTMLElement;
|
||||
private readonly regex: RegexCheckbox;
|
||||
private readonly wholeWords: WholeWordsCheckbox;
|
||||
private readonly 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');
|
||||
|
||||
const inputActiveOptionBorderColor = themeService.getColorTheme().getColor(inputActiveOptionBorder);
|
||||
const inputActiveOptionForegroundColor = themeService.getColorTheme().getColor(inputActiveOptionForeground);
|
||||
const inputActiveOptionBackgroundColor = themeService.getColorTheme().getColor(inputActiveOptionBackground);
|
||||
|
||||
this.caseSensitive = this._register(new CaseSensitiveCheckbox({
|
||||
appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleCaseSensitiveCommand),
|
||||
isChecked: this._state.matchCase,
|
||||
inputActiveOptionBorder: inputActiveOptionBorderColor,
|
||||
inputActiveOptionForeground: inputActiveOptionForegroundColor,
|
||||
inputActiveOptionBackground: inputActiveOptionBackgroundColor
|
||||
}));
|
||||
this._domNode.appendChild(this.caseSensitive.domNode);
|
||||
this._register(this.caseSensitive.onChange(() => {
|
||||
this._state.change({
|
||||
matchCase: this.caseSensitive.checked
|
||||
}, false);
|
||||
}));
|
||||
|
||||
this.wholeWords = this._register(new WholeWordsCheckbox({
|
||||
appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleWholeWordCommand),
|
||||
isChecked: this._state.wholeWord,
|
||||
inputActiveOptionBorder: inputActiveOptionBorderColor,
|
||||
inputActiveOptionForeground: inputActiveOptionForegroundColor,
|
||||
inputActiveOptionBackground: inputActiveOptionBackgroundColor
|
||||
}));
|
||||
this._domNode.appendChild(this.wholeWords.domNode);
|
||||
this._register(this.wholeWords.onChange(() => {
|
||||
this._state.change({
|
||||
wholeWord: this.wholeWords.checked
|
||||
}, false);
|
||||
}));
|
||||
|
||||
this.regex = this._register(new RegexCheckbox({
|
||||
appendTitle: this._keybindingLabelFor(FIND_IDS.ToggleRegexCommand),
|
||||
isChecked: this._state.isRegex,
|
||||
inputActiveOptionBorder: inputActiveOptionBorderColor,
|
||||
inputActiveOptionForeground: inputActiveOptionForegroundColor,
|
||||
inputActiveOptionBackground: inputActiveOptionBackgroundColor
|
||||
}));
|
||||
this._domNode.appendChild(this.regex.domNode);
|
||||
this._register(this.regex.onChange(() => {
|
||||
this._state.change({
|
||||
isRegex: this.regex.checked
|
||||
}, false);
|
||||
}));
|
||||
|
||||
this._editor.addOverlayWidget(this);
|
||||
|
||||
this._register(this._state.onFindReplaceStateChange((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.getColorTheme());
|
||||
this._register(themeService.onDidColorThemeChange(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(), 2000));
|
||||
|
||||
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: IColorTheme) {
|
||||
let inputStyles = {
|
||||
inputActiveOptionBorder: theme.getColor(inputActiveOptionBorder),
|
||||
inputActiveOptionForeground: theme.getColor(inputActiveOptionForeground),
|
||||
inputActiveOptionBackground: theme.getColor(inputActiveOptionBackground)
|
||||
};
|
||||
this.caseSensitive.style(inputStyles);
|
||||
this.wholeWords.style(inputStyles);
|
||||
this.regex.style(inputStyles);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const widgetBackground = theme.getColor(editorWidgetBackground);
|
||||
if (widgetBackground) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { background-color: ${widgetBackground}; }`);
|
||||
}
|
||||
|
||||
const widgetForeground = theme.getColor(editorWidgetForeground);
|
||||
if (widgetForeground) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { color: ${widgetForeground}; }`);
|
||||
}
|
||||
|
||||
|
||||
const widgetShadowColor = theme.getColor(widgetShadow);
|
||||
if (widgetShadowColor) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { box-shadow: 0 2px 8px ${widgetShadowColor}; }`);
|
||||
}
|
||||
|
||||
const hcBorder = theme.getColor(contrastBorder);
|
||||
if (hcBorder) {
|
||||
collector.addRule(`.monaco-editor .findOptionsWidget { border: 2px solid ${hcBorder}; }`);
|
||||
}
|
||||
});
|
||||
299
lib/vscode/src/vs/editor/contrib/find/findState.ts
Normal file
299
lib/vscode/src/vs/editor/contrib/find/findState.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { MATCHES_LIMIT } from './findModel';
|
||||
|
||||
export interface FindReplaceStateChangedEvent {
|
||||
moveCursor: boolean;
|
||||
updateHistory: boolean;
|
||||
|
||||
searchString: boolean;
|
||||
replaceString: boolean;
|
||||
isRevealed: boolean;
|
||||
isReplaceRevealed: boolean;
|
||||
isRegex: boolean;
|
||||
wholeWord: boolean;
|
||||
matchCase: boolean;
|
||||
preserveCase: boolean;
|
||||
searchScope: boolean;
|
||||
matchesPosition: boolean;
|
||||
matchesCount: boolean;
|
||||
currentMatch: boolean;
|
||||
loop: boolean;
|
||||
}
|
||||
|
||||
export const enum FindOptionOverride {
|
||||
NotSet = 0,
|
||||
True = 1,
|
||||
False = 2
|
||||
}
|
||||
|
||||
export interface INewFindReplaceState {
|
||||
searchString?: string;
|
||||
replaceString?: string;
|
||||
isRevealed?: boolean;
|
||||
isReplaceRevealed?: boolean;
|
||||
isRegex?: boolean;
|
||||
isRegexOverride?: FindOptionOverride;
|
||||
wholeWord?: boolean;
|
||||
wholeWordOverride?: FindOptionOverride;
|
||||
matchCase?: boolean;
|
||||
matchCaseOverride?: FindOptionOverride;
|
||||
preserveCase?: boolean;
|
||||
preserveCaseOverride?: FindOptionOverride;
|
||||
searchScope?: Range[] | null;
|
||||
loop?: boolean;
|
||||
}
|
||||
|
||||
function effectiveOptionValue(override: FindOptionOverride, value: boolean): boolean {
|
||||
if (override === FindOptionOverride.True) {
|
||||
return true;
|
||||
}
|
||||
if (override === FindOptionOverride.False) {
|
||||
return false;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export class FindReplaceState extends Disposable {
|
||||
private _searchString: string;
|
||||
private _replaceString: string;
|
||||
private _isRevealed: boolean;
|
||||
private _isReplaceRevealed: boolean;
|
||||
private _isRegex: boolean;
|
||||
private _isRegexOverride: FindOptionOverride;
|
||||
private _wholeWord: boolean;
|
||||
private _wholeWordOverride: FindOptionOverride;
|
||||
private _matchCase: boolean;
|
||||
private _matchCaseOverride: FindOptionOverride;
|
||||
private _preserveCase: boolean;
|
||||
private _preserveCaseOverride: FindOptionOverride;
|
||||
private _searchScope: Range[] | null;
|
||||
private _matchesPosition: number;
|
||||
private _matchesCount: number;
|
||||
private _currentMatch: Range | null;
|
||||
private _loop: boolean;
|
||||
private readonly _onFindReplaceStateChange = this._register(new Emitter<FindReplaceStateChangedEvent>());
|
||||
|
||||
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 effectiveOptionValue(this._isRegexOverride, this._isRegex); }
|
||||
public get wholeWord(): boolean { return effectiveOptionValue(this._wholeWordOverride, this._wholeWord); }
|
||||
public get matchCase(): boolean { return effectiveOptionValue(this._matchCaseOverride, this._matchCase); }
|
||||
public get preserveCase(): boolean { return effectiveOptionValue(this._preserveCaseOverride, this._preserveCase); }
|
||||
|
||||
public get actualIsRegex(): boolean { return this._isRegex; }
|
||||
public get actualWholeWord(): boolean { return this._wholeWord; }
|
||||
public get actualMatchCase(): boolean { return this._matchCase; }
|
||||
public get actualPreserveCase(): boolean { return this._preserveCase; }
|
||||
|
||||
public get searchScope(): Range[] | null { return this._searchScope; }
|
||||
public get matchesPosition(): number { return this._matchesPosition; }
|
||||
public get matchesCount(): number { return this._matchesCount; }
|
||||
public get currentMatch(): Range | null { return this._currentMatch; }
|
||||
public readonly onFindReplaceStateChange: Event<FindReplaceStateChangedEvent> = this._onFindReplaceStateChange.event;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._searchString = '';
|
||||
this._replaceString = '';
|
||||
this._isRevealed = false;
|
||||
this._isReplaceRevealed = false;
|
||||
this._isRegex = false;
|
||||
this._isRegexOverride = FindOptionOverride.NotSet;
|
||||
this._wholeWord = false;
|
||||
this._wholeWordOverride = FindOptionOverride.NotSet;
|
||||
this._matchCase = false;
|
||||
this._matchCaseOverride = FindOptionOverride.NotSet;
|
||||
this._preserveCase = false;
|
||||
this._preserveCaseOverride = FindOptionOverride.NotSet;
|
||||
this._searchScope = null;
|
||||
this._matchesPosition = 0;
|
||||
this._matchesCount = 0;
|
||||
this._currentMatch = null;
|
||||
this._loop = true;
|
||||
}
|
||||
|
||||
public changeMatchInfo(matchesPosition: number, matchesCount: number, currentMatch: Range | undefined): void {
|
||||
let changeEvent: FindReplaceStateChangedEvent = {
|
||||
moveCursor: false,
|
||||
updateHistory: false,
|
||||
searchString: false,
|
||||
replaceString: false,
|
||||
isRevealed: false,
|
||||
isReplaceRevealed: false,
|
||||
isRegex: false,
|
||||
wholeWord: false,
|
||||
matchCase: false,
|
||||
preserveCase: false,
|
||||
searchScope: false,
|
||||
matchesPosition: false,
|
||||
matchesCount: false,
|
||||
currentMatch: false,
|
||||
loop: 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._onFindReplaceStateChange.fire(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,
|
||||
preserveCase: false,
|
||||
searchScope: false,
|
||||
matchesPosition: false,
|
||||
matchesCount: false,
|
||||
currentMatch: false,
|
||||
loop: false
|
||||
};
|
||||
let somethingChanged = false;
|
||||
|
||||
const oldEffectiveIsRegex = this.isRegex;
|
||||
const oldEffectiveWholeWords = this.wholeWord;
|
||||
const oldEffectiveMatchCase = this.matchCase;
|
||||
const oldEffectivePreserveCase = this.preserveCase;
|
||||
|
||||
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') {
|
||||
this._isRegex = newState.isRegex;
|
||||
}
|
||||
if (typeof newState.wholeWord !== 'undefined') {
|
||||
this._wholeWord = newState.wholeWord;
|
||||
}
|
||||
if (typeof newState.matchCase !== 'undefined') {
|
||||
this._matchCase = newState.matchCase;
|
||||
}
|
||||
if (typeof newState.preserveCase !== 'undefined') {
|
||||
this._preserveCase = newState.preserveCase;
|
||||
}
|
||||
if (typeof newState.searchScope !== 'undefined') {
|
||||
if (!newState.searchScope?.every((newSearchScope) => {
|
||||
return this._searchScope?.some(existingSearchScope => {
|
||||
return !Range.equalsRange(existingSearchScope, newSearchScope);
|
||||
});
|
||||
})) {
|
||||
this._searchScope = newState.searchScope;
|
||||
changeEvent.searchScope = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
if (typeof newState.loop !== 'undefined') {
|
||||
if (this._loop !== newState.loop) {
|
||||
this._loop = newState.loop;
|
||||
changeEvent.loop = true;
|
||||
somethingChanged = true;
|
||||
}
|
||||
}
|
||||
// Overrides get set when they explicitly come in and get reset anytime something else changes
|
||||
this._isRegexOverride = (typeof newState.isRegexOverride !== 'undefined' ? newState.isRegexOverride : FindOptionOverride.NotSet);
|
||||
this._wholeWordOverride = (typeof newState.wholeWordOverride !== 'undefined' ? newState.wholeWordOverride : FindOptionOverride.NotSet);
|
||||
this._matchCaseOverride = (typeof newState.matchCaseOverride !== 'undefined' ? newState.matchCaseOverride : FindOptionOverride.NotSet);
|
||||
this._preserveCaseOverride = (typeof newState.preserveCaseOverride !== 'undefined' ? newState.preserveCaseOverride : FindOptionOverride.NotSet);
|
||||
|
||||
if (oldEffectiveIsRegex !== this.isRegex) {
|
||||
somethingChanged = true;
|
||||
changeEvent.isRegex = true;
|
||||
}
|
||||
if (oldEffectiveWholeWords !== this.wholeWord) {
|
||||
somethingChanged = true;
|
||||
changeEvent.wholeWord = true;
|
||||
}
|
||||
if (oldEffectiveMatchCase !== this.matchCase) {
|
||||
somethingChanged = true;
|
||||
changeEvent.matchCase = true;
|
||||
}
|
||||
|
||||
if (oldEffectivePreserveCase !== this.preserveCase) {
|
||||
somethingChanged = true;
|
||||
changeEvent.preserveCase = true;
|
||||
}
|
||||
|
||||
if (somethingChanged) {
|
||||
this._onFindReplaceStateChange.fire(changeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
public canNavigateBack(): boolean {
|
||||
return this.canNavigateInLoop() || (this.matchesPosition !== 1);
|
||||
}
|
||||
|
||||
public canNavigateForward(): boolean {
|
||||
return this.canNavigateInLoop() || (this.matchesPosition < this.matchesCount);
|
||||
}
|
||||
|
||||
private canNavigateInLoop(): boolean {
|
||||
return this._loop || (this.matchesCount >= MATCHES_LIMIT);
|
||||
}
|
||||
|
||||
}
|
||||
213
lib/vscode/src/vs/editor/contrib/find/findWidget.css
Normal file
213
lib/vscode/src/vs/editor/contrib/find/findWidget.css
Normal file
@@ -0,0 +1,213 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Find widget */
|
||||
.monaco-editor .find-widget {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
height: 33px;
|
||||
overflow: hidden;
|
||||
line-height: 19px;
|
||||
transition: transform 200ms linear;
|
||||
padding: 0 4px;
|
||||
box-sizing: border-box;
|
||||
transform: translateY(calc(-100% - 10px)); /* shadow (10px) */
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget textarea {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.hiddenEditor {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Find widget when replace is toggled on */
|
||||
.monaco-editor .find-widget.replaceToggled > .replace-part {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-inputbox.synthetic-focus {
|
||||
outline: 1px solid -webkit-focus-ring-color;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-inputbox .input {
|
||||
background-color: transparent;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-findInput .input {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part,
|
||||
.monaco-editor .find-widget > .replace-part {
|
||||
margin: 4px 0 0 17px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part .monaco-inputbox,
|
||||
.monaco-editor .find-widget > .replace-part .monaco-inputbox {
|
||||
min-height: 25px;
|
||||
}
|
||||
|
||||
|
||||
.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror {
|
||||
padding-right: 22px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .input,
|
||||
.monaco-editor .find-widget > .find-part .monaco-inputbox > .wrapper > .mirror,
|
||||
.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .input,
|
||||
.monaco-editor .find-widget > .replace-part .monaco-inputbox > .wrapper > .mirror {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .find-part .find-actions {
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .replace-part .replace-actions {
|
||||
height: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-findInput {
|
||||
vertical-align: middle;
|
||||
display: flex;
|
||||
flex:1;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-findInput .monaco-scrollable-element {
|
||||
/* Make sure textarea inherits the width correctly */
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-findInput .monaco-scrollable-element .scrollbar.vertical {
|
||||
/* Hide vertical scrollbar */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .matchesCount {
|
||||
display: flex;
|
||||
flex: initial;
|
||||
margin: 0 0 0 3px;
|
||||
padding: 2px 0 0 2px;
|
||||
height: 25px;
|
||||
vertical-align: middle;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
flex: initial;
|
||||
margin-left: 3px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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: 3px;
|
||||
width: 18px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .button.toggle.disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .disabled {
|
||||
opacity: 0.3;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .replace-part {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .replace-part > .monaco-findInput {
|
||||
position: relative;
|
||||
display: flex;
|
||||
vertical-align: middle;
|
||||
flex: auto;
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget > .replace-part > .monaco-findInput > .controls {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
/* REDUCED */
|
||||
.monaco-editor .find-widget.reduced-find-widget .matchesCount {
|
||||
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: 170px !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 .findMatch {
|
||||
animation-duration: 0;
|
||||
animation-name: inherit !important;
|
||||
}
|
||||
|
||||
.monaco-editor .find-widget .monaco-sash {
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
.monaco-editor.hc-black .find-widget .button:before {
|
||||
position: relative;
|
||||
top: 1px;
|
||||
left: 2px;
|
||||
}
|
||||
1397
lib/vscode/src/vs/editor/contrib/find/findWidget.ts
Normal file
1397
lib/vscode/src/vs/editor/contrib/find/findWidget.ts
Normal file
File diff suppressed because it is too large
Load Diff
72
lib/vscode/src/vs/editor/contrib/find/replaceAllCommand.ts
Normal file
72
lib/vscode/src/vs/editor/contrib/find/replaceAllCommand.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { ICommand, IEditOperationBuilder, ICursorStateComputerData } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
|
||||
interface IEditOperation {
|
||||
range: Range;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export class ReplaceAllCommand implements ICommand {
|
||||
|
||||
private readonly _editorSelection: Selection;
|
||||
private _trackedEditorSelectionId: string | null;
|
||||
private readonly _ranges: Range[];
|
||||
private readonly _replaceStrings: string[];
|
||||
|
||||
constructor(editorSelection: Selection, ranges: Range[], replaceStrings: string[]) {
|
||||
this._editorSelection = editorSelection;
|
||||
this._ranges = ranges;
|
||||
this._replaceStrings = replaceStrings;
|
||||
this._trackedEditorSelectionId = null;
|
||||
}
|
||||
|
||||
public getEditOperations(model: ITextModel, builder: IEditOperationBuilder): void {
|
||||
if (this._ranges.length > 0) {
|
||||
// Collect all edit operations
|
||||
let ops: IEditOperation[] = [];
|
||||
for (let 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
|
||||
let resultOps: IEditOperation[] = [];
|
||||
let previousOp = ops[0];
|
||||
for (let 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 (const op of resultOps) {
|
||||
builder.addEditOperation(op.range, op.text);
|
||||
}
|
||||
}
|
||||
|
||||
this._trackedEditorSelectionId = builder.trackSelection(this._editorSelection);
|
||||
}
|
||||
|
||||
public computeCursorState(model: ITextModel, helper: ICursorStateComputerData): Selection {
|
||||
return helper.getTrackedSelection(this._trackedEditorSelectionId!);
|
||||
}
|
||||
}
|
||||
347
lib/vscode/src/vs/editor/contrib/find/replacePattern.ts
Normal file
347
lib/vscode/src/vs/editor/contrib/find/replacePattern.ts
Normal file
@@ -0,0 +1,347 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CharCode } from 'vs/base/common/charCode';
|
||||
import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search';
|
||||
|
||||
const enum ReplacePatternKind {
|
||||
StaticValue = 0,
|
||||
DynamicPieces = 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigned when the replace pattern is entirely static.
|
||||
*/
|
||||
class StaticValueReplacePattern {
|
||||
public readonly kind = ReplacePatternKind.StaticValue;
|
||||
constructor(public readonly staticValue: string) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigned when the replace pattern has replacement patterns.
|
||||
*/
|
||||
class DynamicPiecesReplacePattern {
|
||||
public readonly kind = ReplacePatternKind.DynamicPieces;
|
||||
constructor(public readonly pieces: ReplacePiece[]) { }
|
||||
}
|
||||
|
||||
export class ReplacePattern {
|
||||
|
||||
public static fromStaticValue(value: string): ReplacePattern {
|
||||
return new ReplacePattern([ReplacePiece.staticValue(value)]);
|
||||
}
|
||||
|
||||
private readonly _state: StaticValueReplacePattern | DynamicPiecesReplacePattern;
|
||||
|
||||
public get hasReplacementPatterns(): boolean {
|
||||
return (this._state.kind === ReplacePatternKind.DynamicPieces);
|
||||
}
|
||||
|
||||
constructor(pieces: ReplacePiece[] | null) {
|
||||
if (!pieces || pieces.length === 0) {
|
||||
this._state = new StaticValueReplacePattern('');
|
||||
} else if (pieces.length === 1 && pieces[0].staticValue !== null) {
|
||||
this._state = new StaticValueReplacePattern(pieces[0].staticValue);
|
||||
} else {
|
||||
this._state = new DynamicPiecesReplacePattern(pieces);
|
||||
}
|
||||
}
|
||||
|
||||
public buildReplaceString(matches: string[] | null, preserveCase?: boolean): string {
|
||||
if (this._state.kind === ReplacePatternKind.StaticValue) {
|
||||
if (preserveCase) {
|
||||
return buildReplaceStringWithCasePreserved(matches, this._state.staticValue);
|
||||
} else {
|
||||
return this._state.staticValue;
|
||||
}
|
||||
}
|
||||
|
||||
let result = '';
|
||||
for (let i = 0, len = this._state.pieces.length; i < len; i++) {
|
||||
let piece = this._state.pieces[i];
|
||||
if (piece.staticValue !== null) {
|
||||
// static value ReplacePiece
|
||||
result += piece.staticValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// match index ReplacePiece
|
||||
let match: string = ReplacePattern._substitute(piece.matchIndex, matches);
|
||||
if (piece.caseOps !== null && piece.caseOps.length > 0) {
|
||||
let repl: string[] = [];
|
||||
let lenOps: number = piece.caseOps.length;
|
||||
let opIdx: number = 0;
|
||||
for (let idx: number = 0, len: number = match.length; idx < len; idx++) {
|
||||
if (opIdx >= lenOps) {
|
||||
repl.push(match.slice(idx));
|
||||
break;
|
||||
}
|
||||
switch (piece.caseOps[opIdx]) {
|
||||
case 'U':
|
||||
repl.push(match[idx].toUpperCase());
|
||||
break;
|
||||
case 'u':
|
||||
repl.push(match[idx].toUpperCase());
|
||||
opIdx++;
|
||||
break;
|
||||
case 'L':
|
||||
repl.push(match[idx].toLowerCase());
|
||||
break;
|
||||
case 'l':
|
||||
repl.push(match[idx].toLowerCase());
|
||||
opIdx++;
|
||||
break;
|
||||
default:
|
||||
repl.push(match[idx]);
|
||||
}
|
||||
}
|
||||
match = repl.join('');
|
||||
}
|
||||
result += match;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static _substitute(matchIndex: number, matches: string[] | null): string {
|
||||
if (matches === null) {
|
||||
return '';
|
||||
}
|
||||
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, null);
|
||||
}
|
||||
|
||||
public static matchIndex(index: number): ReplacePiece {
|
||||
return new ReplacePiece(null, index, null);
|
||||
}
|
||||
|
||||
public static caseOps(index: number, caseOps: string[]): ReplacePiece {
|
||||
return new ReplacePiece(null, index, caseOps);
|
||||
}
|
||||
|
||||
public readonly staticValue: string | null;
|
||||
public readonly matchIndex: number;
|
||||
public readonly caseOps: string[] | null;
|
||||
|
||||
private constructor(staticValue: string | null, matchIndex: number, caseOps: string[] | null) {
|
||||
this.staticValue = staticValue;
|
||||
this.matchIndex = matchIndex;
|
||||
if (!caseOps || caseOps.length === 0) {
|
||||
this.caseOps = null;
|
||||
} else {
|
||||
this.caseOps = caseOps.slice(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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, caseOps: string[]): void {
|
||||
if (this._currentStaticPiece.length !== 0) {
|
||||
this._result[this._resultLen++] = ReplacePiece.staticValue(this._currentStaticPiece);
|
||||
this._currentStaticPiece = '';
|
||||
}
|
||||
this._result[this._resultLen++] = ReplacePiece.caseOps(index, caseOps);
|
||||
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 "\".
|
||||
* \u => upper-cases one character in a match.
|
||||
* \U => upper-cases ALL remaining characters in a match.
|
||||
* \l => lower-cases one character in a match.
|
||||
* \L => lower-cases ALL remaining characters in a match.
|
||||
* $$ => 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 caseOps: string[] = [];
|
||||
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 = 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;
|
||||
// Case modification of string replacements, patterned after Boost, but only applied
|
||||
// to the replacement text, not subsequent content.
|
||||
case CharCode.u:
|
||||
// \u => upper-cases one character.
|
||||
case CharCode.U:
|
||||
// \U => upper-cases ALL following characters.
|
||||
case CharCode.l:
|
||||
// \l => lower-cases one character.
|
||||
case CharCode.L:
|
||||
// \L => lower-cases ALL following characters.
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitStatic('', i + 1);
|
||||
caseOps.push(String.fromCharCode(nextChCode));
|
||||
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, caseOps);
|
||||
caseOps.length = 0;
|
||||
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, caseOps);
|
||||
caseOps.length = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
result.emitUnchanged(i - 1);
|
||||
result.emitMatchIndex(matchIndex, i + 1, caseOps);
|
||||
caseOps.length = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.finalize();
|
||||
}
|
||||
86
lib/vscode/src/vs/editor/contrib/find/test/find.test.ts
Normal file
86
lib/vscode/src/vs/editor/contrib/find/test/find.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
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/findController';
|
||||
import { withTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
|
||||
|
||||
suite('Find', () => {
|
||||
|
||||
test('search string at position', () => {
|
||||
withTestCodeEditor([
|
||||
'ABC DEF',
|
||||
'0123 456'
|
||||
], {}, (editor) => {
|
||||
|
||||
// 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', () => {
|
||||
withTestCodeEditor([
|
||||
'ABC DEF',
|
||||
'0123 456'
|
||||
], {}, (editor) => {
|
||||
|
||||
// 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', () => {
|
||||
withTestCodeEditor([
|
||||
'ABC DEF',
|
||||
'0123 456'
|
||||
], {}, (editor) => {
|
||||
|
||||
// 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 chunk of second
|
||||
editor.setSelection(new Range(1, 7, 2, 4));
|
||||
let searchStringSelectionSpanLines = getSelectionSearchString(editor);
|
||||
assert.equal(searchStringSelectionSpanLines, null);
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,621 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { Delayer } from 'vs/base/common/async';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
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 { CommonFindController, FindStartFocusAction, IFindStartOptions, NextMatchFindAction, NextSelectionMatchFindAction, StartFindAction, StartFindReplaceAction } from 'vs/editor/contrib/find/findController';
|
||||
import { CONTEXT_FIND_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel';
|
||||
import { withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor';
|
||||
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
|
||||
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
export class TestFindController extends CommonFindController {
|
||||
|
||||
public hasFocus: boolean;
|
||||
public delayUpdateHistory: boolean = false;
|
||||
|
||||
private _findInputFocused: IContextKey<boolean>;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IClipboardService clipboardService: IClipboardService
|
||||
) {
|
||||
super(editor, contextKeyService, storageService, clipboardService);
|
||||
this._findInputFocused = CONTEXT_FIND_INPUT_FOCUSED.bindTo(contextKeyService);
|
||||
this._updateHistoryDelayer = new Delayer<void>(50);
|
||||
this.hasFocus = false;
|
||||
}
|
||||
|
||||
protected async _start(opts: IFindStartOptions): Promise<void> {
|
||||
await super._start(opts);
|
||||
|
||||
if (opts.shouldFocus !== FindStartFocusAction.NoFocusChange) {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
|
||||
let inputFocused = opts.shouldFocus === FindStartFocusAction.FocusFindInput;
|
||||
this._findInputFocused.set(inputFocused);
|
||||
}
|
||||
}
|
||||
|
||||
function fromSelection(slc: Selection): number[] {
|
||||
return [slc.startLineNumber, slc.startColumn, slc.endLineNumber, slc.endColumn];
|
||||
}
|
||||
|
||||
suite('FindController', async () => {
|
||||
let queryState: { [key: string]: any; } = {};
|
||||
let clipboardState = '';
|
||||
let serviceCollection = new ServiceCollection();
|
||||
serviceCollection.set(IStorageService, {
|
||||
_serviceBrand: undefined,
|
||||
onDidChangeStorage: Event.None,
|
||||
onWillSaveState: Event.None,
|
||||
get: (key: string) => queryState[key],
|
||||
getBoolean: (key: string) => !!queryState[key],
|
||||
getNumber: (key: string) => undefined,
|
||||
store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); },
|
||||
remove: () => undefined
|
||||
} as any);
|
||||
|
||||
if (platform.isMacintosh) {
|
||||
serviceCollection.set(IClipboardService, <any>{
|
||||
readFindText: () => clipboardState,
|
||||
writeFindText: (value: any) => { clipboardState = value; }
|
||||
});
|
||||
}
|
||||
|
||||
/* test('stores to the global clipboard buffer on start find action', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'ABC',
|
||||
'ABC',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
if (!platform.isMacintosh) {
|
||||
assert.ok(true);
|
||||
return;
|
||||
}
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
let startFindAction = new StartFindAction();
|
||||
// I select ABC on the first line
|
||||
editor.setSelection(new Selection(1, 1, 1, 4));
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
startFindAction.run(null, editor);
|
||||
|
||||
assert.deepEqual(findController.getGlobalBufferTerm(), findController.getState().searchString);
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('reads from the global clipboard buffer on next find action if buffer exists', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'ABC',
|
||||
'ABC',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = 'ABC';
|
||||
|
||||
if (!platform.isMacintosh) {
|
||||
assert.ok(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
let findState = findController.getState();
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
nextMatchFindAction.run(null, editor);
|
||||
assert.equal(findState.searchString, 'ABC');
|
||||
|
||||
assert.deepEqual(fromSelection(editor.getSelection()!), [1, 1, 1, 4]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('writes to the global clipboard buffer when text changes', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'ABC',
|
||||
'ABC',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
if (!platform.isMacintosh) {
|
||||
assert.ok(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
let findState = findController.getState();
|
||||
|
||||
findState.change({ searchString: 'ABC' }, true);
|
||||
|
||||
assert.deepEqual(findController.getGlobalBufferTerm(), 'ABC');
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
}); */
|
||||
|
||||
test('issue #1857: F3, Find Next, acts like "Find Under Cursor"', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'ABC',
|
||||
'ABC',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
// The cursor is at the very top, of the file, at the first ABC
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
let findState = findController.getState();
|
||||
let startFindAction = new StartFindAction();
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
await 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(fromSelection(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(fromSelection(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(fromSelection(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.
|
||||
await 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', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'import nls = require(\'vs/nls\');'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
editor.setPosition({
|
||||
lineNumber: 1,
|
||||
column: 9
|
||||
});
|
||||
|
||||
await nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromSelection(editor.getSelection()!), [1, 26, 1, 29]);
|
||||
|
||||
await nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromSelection(editor.getSelection()!), [1, 8, 1, 11]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #6149: Auto-escape highlighted text for search and replace regex mode', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
let startFindAction = new StartFindAction();
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
|
||||
editor.setSelection(new Selection(1, 9, 1, 13));
|
||||
|
||||
findController.toggleRegex();
|
||||
await startFindAction.run(null, editor);
|
||||
|
||||
await nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromSelection(editor.getSelection()!), [2, 9, 2, 13]);
|
||||
|
||||
await nextMatchFindAction.run(null, editor);
|
||||
assert.deepEqual(fromSelection(editor.getSelection()!), [1, 9, 1, 13]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #41027: Don\'t replace find input value on replace action if find input is active', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'test',
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
let testRegexString = 'tes.';
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
let nextMatchFindAction = new NextMatchFindAction();
|
||||
let startFindReplaceAction = new StartFindReplaceAction();
|
||||
|
||||
findController.toggleRegex();
|
||||
findController.setSearchString(testRegexString);
|
||||
await findController.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: false,
|
||||
seedSearchStringFromGlobalClipboard: false,
|
||||
shouldFocus: FindStartFocusAction.FocusFindInput,
|
||||
shouldAnimate: false,
|
||||
updateSearchScope: false,
|
||||
loop: true
|
||||
});
|
||||
await nextMatchFindAction.run(null, editor);
|
||||
await startFindReplaceAction.run(null, editor);
|
||||
|
||||
assert.equal(findController.getState().searchString, testRegexString);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #9043: Clear search scope when find widget is hidden', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
await findController.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: false,
|
||||
seedSearchStringFromGlobalClipboard: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: false,
|
||||
updateSearchScope: false,
|
||||
loop: true
|
||||
});
|
||||
|
||||
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('issue #18111: Regex replace with single space replaces with no space', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'HRESULT OnAmbientPropertyChange(DISPID dispid);'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
|
||||
let startFindAction = new StartFindAction();
|
||||
await startFindAction.run(null, editor);
|
||||
|
||||
findController.getState().change({ searchString: '\\b\\s{3}\\b', replaceString: ' ', isRegex: true }, false);
|
||||
findController.moveToNextMatch();
|
||||
|
||||
assert.deepEqual(editor.getSelections()!.map(fromSelection), [
|
||||
[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', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'',
|
||||
'line2',
|
||||
'line3'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
|
||||
let startFindAction = new StartFindAction();
|
||||
await startFindAction.run(null, editor);
|
||||
|
||||
findController.getState().change({ searchString: '^', replaceString: 'x', isRegex: true }, false);
|
||||
findController.moveToNextMatch();
|
||||
|
||||
assert.deepEqual(editor.getSelections()!.map(fromSelection), [
|
||||
[2, 1, 2, 1]
|
||||
]);
|
||||
|
||||
findController.replace();
|
||||
|
||||
assert.deepEqual(editor.getValue(), '\nxline2\nline3');
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #38232: Find Next Selection, regex enabled', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'([funny]',
|
||||
'',
|
||||
'([funny]'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
let nextSelectionMatchFindAction = new NextSelectionMatchFindAction();
|
||||
|
||||
// toggle regex
|
||||
findController.getState().change({ isRegex: true }, false);
|
||||
|
||||
// change selection
|
||||
editor.setSelection(new Selection(1, 1, 1, 9));
|
||||
|
||||
// cmd+f3
|
||||
await nextSelectionMatchFindAction.run(null, editor);
|
||||
|
||||
assert.deepEqual(editor.getSelections()!.map(fromSelection), [
|
||||
[3, 1, 3, 9]
|
||||
]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #38232: Find Next Selection, regex enabled, find widget open', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'([funny]',
|
||||
'',
|
||||
'([funny]'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
clipboardState = '';
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
let startFindAction = new StartFindAction();
|
||||
let nextSelectionMatchFindAction = new NextSelectionMatchFindAction();
|
||||
|
||||
// cmd+f - open find widget
|
||||
await startFindAction.run(null, editor);
|
||||
|
||||
// toggle regex
|
||||
findController.getState().change({ isRegex: true }, false);
|
||||
|
||||
// change selection
|
||||
editor.setSelection(new Selection(1, 1, 1, 9));
|
||||
|
||||
// cmd+f3
|
||||
await nextSelectionMatchFindAction.run(null, editor);
|
||||
|
||||
assert.deepEqual(editor.getSelections()!.map(fromSelection), [
|
||||
[3, 1, 3, 9]
|
||||
]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('FindController query options persistence', async () => {
|
||||
let queryState: { [key: string]: any; } = {};
|
||||
queryState['editor.isRegex'] = false;
|
||||
queryState['editor.matchCase'] = false;
|
||||
queryState['editor.wholeWord'] = false;
|
||||
let serviceCollection = new ServiceCollection();
|
||||
serviceCollection.set(IStorageService, {
|
||||
_serviceBrand: undefined,
|
||||
onDidChangeStorage: Event.None,
|
||||
onWillSaveState: Event.None,
|
||||
get: (key: string) => queryState[key],
|
||||
getBoolean: (key: string) => !!queryState[key],
|
||||
getNumber: (key: string) => undefined,
|
||||
store: (key: string, value: any) => { queryState[key] = value; return Promise.resolve(); },
|
||||
remove: () => undefined
|
||||
} as any);
|
||||
|
||||
test('matchCase', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'abc',
|
||||
'ABC',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
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.ID, TestFindController);
|
||||
let findState = findController.getState();
|
||||
let startFindAction = new StartFindAction();
|
||||
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
await startFindAction.run(null, editor);
|
||||
|
||||
// I type ABC.
|
||||
findState.change({ searchString: 'ABC' }, true);
|
||||
// The second ABC is highlighted as matchCase is true.
|
||||
assert.deepEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 4]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
queryState = { 'editor.isRegex': false, 'editor.matchCase': false, 'editor.wholeWord': true };
|
||||
|
||||
test('wholeWord', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'ABC',
|
||||
'AB',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
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.ID, TestFindController);
|
||||
let findState = findController.getState();
|
||||
let startFindAction = new StartFindAction();
|
||||
|
||||
// I hit Ctrl+F to show the Find dialog
|
||||
await startFindAction.run(null, editor);
|
||||
|
||||
// I type AB.
|
||||
findState.change({ searchString: 'AB' }, true);
|
||||
// The second AB is highlighted as wholeWord is true.
|
||||
assert.deepEqual(fromSelection(editor.getSelection()!), [2, 1, 2, 3]);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('toggling options is saved', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'ABC',
|
||||
'AB',
|
||||
'XYZ',
|
||||
'ABC'
|
||||
], { serviceCollection: serviceCollection }, async (editor) => {
|
||||
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.ID, TestFindController);
|
||||
findController.toggleRegex();
|
||||
assert.equal(queryState['editor.isRegex'], true);
|
||||
|
||||
findController.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #27083: Update search scope once find widget becomes visible', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, async (editor) => {
|
||||
// clipboardState = '';
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
const findConfig = {
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: false,
|
||||
seedSearchStringFromGlobalClipboard: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: false,
|
||||
updateSearchScope: true,
|
||||
loop: true
|
||||
};
|
||||
|
||||
editor.setSelection(new Range(1, 1, 2, 1));
|
||||
findController.start(findConfig);
|
||||
assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1)]);
|
||||
|
||||
findController.closeFindWidget();
|
||||
|
||||
editor.setSelections([new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]);
|
||||
findController.start(findConfig);
|
||||
assert.deepEqual(findController.getState().searchScope, [new Selection(1, 1, 2, 1), new Selection(2, 1, 2, 5)]);
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #58604: Do not update searchScope if it is empty', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, async (editor) => {
|
||||
// clipboardState = '';
|
||||
editor.setSelection(new Range(1, 2, 1, 2));
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
|
||||
await findController.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: false,
|
||||
seedSearchStringFromGlobalClipboard: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: false,
|
||||
updateSearchScope: true,
|
||||
loop: true
|
||||
});
|
||||
|
||||
assert.deepEqual(findController.getState().searchScope, null);
|
||||
});
|
||||
});
|
||||
|
||||
test('issue #58604: Update searchScope if it is not empty', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'always', globalFindClipboard: false } }, async (editor) => {
|
||||
// clipboardState = '';
|
||||
editor.setSelection(new Range(1, 2, 1, 3));
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
|
||||
await findController.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: false,
|
||||
seedSearchStringFromGlobalClipboard: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: false,
|
||||
updateSearchScope: true,
|
||||
loop: true
|
||||
});
|
||||
|
||||
assert.deepEqual(findController.getState().searchScope, [new Selection(1, 2, 1, 3)]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('issue #27083: Find in selection when multiple lines are selected', async () => {
|
||||
await withAsyncTestCodeEditor([
|
||||
'var x = (3 * 5)',
|
||||
'var y = (3 * 5)',
|
||||
'var z = (3 * 5)',
|
||||
], { serviceCollection: serviceCollection, find: { autoFindInSelection: 'multiline', globalFindClipboard: false } }, async (editor) => {
|
||||
// clipboardState = '';
|
||||
editor.setSelection(new Range(1, 6, 2, 1));
|
||||
let findController = editor.registerAndInstantiateContribution(TestFindController.ID, TestFindController);
|
||||
|
||||
await findController.start({
|
||||
forceRevealReplace: false,
|
||||
seedSearchStringFromSelection: false,
|
||||
seedSearchStringFromGlobalClipboard: false,
|
||||
shouldFocus: FindStartFocusAction.NoFocusChange,
|
||||
shouldAnimate: false,
|
||||
updateSearchScope: true,
|
||||
loop: true
|
||||
});
|
||||
|
||||
assert.deepEqual(findController.getState().searchScope, [new Selection(1, 6, 2, 1)]);
|
||||
});
|
||||
});
|
||||
});
|
||||
2377
lib/vscode/src/vs/editor/contrib/find/test/findModel.test.ts
Normal file
2377
lib/vscode/src/vs/editor/contrib/find/test/findModel.test.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,245 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { ReplacePattern, ReplacePiece, parseReplaceString } from 'vs/editor/contrib/find/replacePattern';
|
||||
import { buildReplaceStringWithCasePreserved } from 'vs/base/common/search';
|
||||
|
||||
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('parse replace string with case modifiers', () => {
|
||||
let testParse = (input: string, expectedPieces: ReplacePiece[]) => {
|
||||
let actual = parseReplaceString(input);
|
||||
let expected = new ReplacePattern(expectedPieces);
|
||||
assert.deepEqual(actual, expected, 'Parsing ' + input);
|
||||
};
|
||||
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}`);
|
||||
}
|
||||
|
||||
// \U, \u => uppercase \L, \l => lowercase \E => cancel
|
||||
|
||||
testParse('hello\\U$1', [ReplacePiece.staticValue('hello'), ReplacePiece.caseOps(1, ['U'])]);
|
||||
assertReplace('func privateFunc(', /func (\w+)\(/, 'func \\U$1(', 'func PRIVATEFUNC(');
|
||||
|
||||
testParse('hello\\u$1', [ReplacePiece.staticValue('hello'), ReplacePiece.caseOps(1, ['u'])]);
|
||||
assertReplace('func privateFunc(', /func (\w+)\(/, 'func \\u$1(', 'func PrivateFunc(');
|
||||
|
||||
testParse('hello\\L$1', [ReplacePiece.staticValue('hello'), ReplacePiece.caseOps(1, ['L'])]);
|
||||
assertReplace('func privateFunc(', /func (\w+)\(/, 'func \\L$1(', 'func privatefunc(');
|
||||
|
||||
testParse('hello\\l$1', [ReplacePiece.staticValue('hello'), ReplacePiece.caseOps(1, ['l'])]);
|
||||
assertReplace('func PrivateFunc(', /func (\w+)\(/, 'func \\l$1(', 'func privateFunc(');
|
||||
|
||||
testParse('hello$1\\u\\u\\U$4goodbye', [ReplacePiece.staticValue('hello'), ReplacePiece.matchIndex(1), ReplacePiece.caseOps(4, ['u', 'u', 'U']), ReplacePiece.staticValue('goodbye')]);
|
||||
assertReplace('hellogooDbye', /hello(\w+)/, 'hello\\u\\u\\l\\l\\U$1', 'helloGOodBYE');
|
||||
});
|
||||
|
||||
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{}');
|
||||
});
|
||||
|
||||
test('buildReplaceStringWithCasePreserved test', () => {
|
||||
function assertReplace(target: string[], replaceString: string, expected: string): void {
|
||||
let actual: string = '';
|
||||
actual = buildReplaceStringWithCasePreserved(target, replaceString);
|
||||
assert.equal(actual, expected);
|
||||
}
|
||||
|
||||
assertReplace(['abc'], 'Def', 'def');
|
||||
assertReplace(['Abc'], 'Def', 'Def');
|
||||
assertReplace(['ABC'], 'Def', 'DEF');
|
||||
assertReplace(['abc', 'Abc'], 'Def', 'def');
|
||||
assertReplace(['Abc', 'abc'], 'Def', 'Def');
|
||||
assertReplace(['ABC', 'abc'], 'Def', 'DEF');
|
||||
assertReplace(['AbC'], 'Def', 'Def');
|
||||
assertReplace(['aBC'], 'Def', 'Def');
|
||||
assertReplace(['Foo-Bar'], 'newfoo-newbar', 'Newfoo-Newbar');
|
||||
assertReplace(['Foo-Bar-Abc'], 'newfoo-newbar-newabc', 'Newfoo-Newbar-Newabc');
|
||||
assertReplace(['Foo-Bar-abc'], 'newfoo-newbar', 'Newfoo-newbar');
|
||||
assertReplace(['foo-Bar'], 'newfoo-newbar', 'newfoo-Newbar');
|
||||
assertReplace(['foo-BAR'], 'newfoo-newbar', 'newfoo-NEWBAR');
|
||||
assertReplace(['Foo_Bar'], 'newfoo_newbar', 'Newfoo_Newbar');
|
||||
assertReplace(['Foo_Bar_Abc'], 'newfoo_newbar_newabc', 'Newfoo_Newbar_Newabc');
|
||||
assertReplace(['Foo_Bar_abc'], 'newfoo_newbar', 'Newfoo_newbar');
|
||||
assertReplace(['Foo_Bar-abc'], 'newfoo_newbar-abc', 'Newfoo_newbar-abc');
|
||||
assertReplace(['foo_Bar'], 'newfoo_newbar', 'newfoo_Newbar');
|
||||
assertReplace(['Foo_BAR'], 'newfoo_newbar', 'Newfoo_NEWBAR');
|
||||
});
|
||||
|
||||
test('preserve case', () => {
|
||||
function assertReplace(target: string[], replaceString: string, expected: string): void {
|
||||
let replacePattern = parseReplaceString(replaceString);
|
||||
let actual = replacePattern.buildReplaceString(target, true);
|
||||
assert.equal(actual, expected);
|
||||
}
|
||||
|
||||
assertReplace(['abc'], 'Def', 'def');
|
||||
assertReplace(['Abc'], 'Def', 'Def');
|
||||
assertReplace(['ABC'], 'Def', 'DEF');
|
||||
assertReplace(['abc', 'Abc'], 'Def', 'def');
|
||||
assertReplace(['Abc', 'abc'], 'Def', 'Def');
|
||||
assertReplace(['ABC', 'abc'], 'Def', 'DEF');
|
||||
assertReplace(['AbC'], 'Def', 'Def');
|
||||
assertReplace(['aBC'], 'Def', 'Def');
|
||||
assertReplace(['Foo-Bar'], 'newfoo-newbar', 'Newfoo-Newbar');
|
||||
assertReplace(['Foo-Bar-Abc'], 'newfoo-newbar-newabc', 'Newfoo-Newbar-Newabc');
|
||||
assertReplace(['Foo-Bar-abc'], 'newfoo-newbar', 'Newfoo-newbar');
|
||||
assertReplace(['foo-Bar'], 'newfoo-newbar', 'newfoo-Newbar');
|
||||
assertReplace(['foo-BAR'], 'newfoo-newbar', 'newfoo-NEWBAR');
|
||||
assertReplace(['Foo_Bar'], 'newfoo_newbar', 'Newfoo_Newbar');
|
||||
assertReplace(['Foo_Bar_Abc'], 'newfoo_newbar_newabc', 'Newfoo_Newbar_Newabc');
|
||||
assertReplace(['Foo_Bar_abc'], 'newfoo_newbar', 'Newfoo_newbar');
|
||||
assertReplace(['Foo_Bar-abc'], 'newfoo_newbar-abc', 'Newfoo_newbar-abc');
|
||||
assertReplace(['foo_Bar'], 'newfoo_newbar', 'newfoo_Newbar');
|
||||
assertReplace(['foo_BAR'], 'newfoo_newbar', 'newfoo_NEWBAR');
|
||||
});
|
||||
});
|
||||
31
lib/vscode/src/vs/editor/contrib/folding/folding.css
Normal file
31
lib/vscode/src/vs/editor/contrib/folding/folding.css
Normal file
@@ -0,0 +1,31 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .margin-view-overlays .codicon-folding-expanded,
|
||||
.monaco-editor .margin-view-overlays .codicon-folding-collapsed {
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 140%;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.monaco-editor .margin-view-overlays:hover .codicon,
|
||||
.monaco-editor .margin-view-overlays .codicon.codicon-folding-collapsed,
|
||||
.monaco-editor .margin-view-overlays .codicon.alwaysShowFoldIcons {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.monaco-editor .inline-folded:after {
|
||||
color: grey;
|
||||
margin: 0.1em 0.2em 0 0.2em;
|
||||
content: "⋯";
|
||||
display: inline;
|
||||
line-height: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
925
lib/vscode/src/vs/editor/contrib/folding/folding.ts
Normal file
925
lib/vscode/src/vs/editor/contrib/folding/folding.ts
Normal file
@@ -0,0 +1,925 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./folding';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as types from 'vs/base/common/types';
|
||||
import { escapeRegExpCharacters } from 'vs/base/common/strings';
|
||||
import { RunOnceScheduler, Delayer, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ScrollType, IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, registerInstantiatedEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { FoldingModel, setCollapseStateAtLevel, CollapseMemento, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines, setCollapseStateForType, toggleCollapseState, setCollapseStateUp } from 'vs/editor/contrib/folding/foldingModel';
|
||||
import { FoldingDecorationProvider, foldingCollapsedIcon, foldingExpandedIcon } from './foldingDecorations';
|
||||
import { FoldingRegions, FoldingRegion } from './foldingRanges';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IMarginData, IEmptyContentData } from 'vs/editor/browser/controller/mouseTarget';
|
||||
import { HiddenRangeModel } from 'vs/editor/contrib/folding/hiddenRangeModel';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { IndentRangeProvider } from 'vs/editor/contrib/folding/indentRangeProvider';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { FoldingRangeProviderRegistry, FoldingRangeKind } from 'vs/editor/common/modes';
|
||||
import { SyntaxRangeProvider, ID_SYNTAX_PROVIDER } from './syntaxRangeProvider';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { InitializingRangeProvider, ID_INIT_PROVIDER } from 'vs/editor/contrib/folding/intializingRangeProvider';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { registerColor, editorSelectionBackground, transparent, iconForeground } from 'vs/platform/theme/common/colorRegistry';
|
||||
|
||||
const CONTEXT_FOLDING_ENABLED = new RawContextKey<boolean>('foldingEnabled', false);
|
||||
|
||||
export interface RangeProvider {
|
||||
readonly id: string;
|
||||
compute(cancelationToken: CancellationToken): Promise<FoldingRegions | null>;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
interface FoldingStateMemento {
|
||||
collapsedRegions?: CollapseMemento;
|
||||
lineCount?: number;
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
export class FoldingController extends Disposable implements IEditorContribution {
|
||||
|
||||
public static ID = 'editor.contrib.folding';
|
||||
|
||||
static readonly MAX_FOLDING_REGIONS = 5000;
|
||||
|
||||
public static get(editor: ICodeEditor): FoldingController {
|
||||
return editor.getContribution<FoldingController>(FoldingController.ID);
|
||||
}
|
||||
|
||||
private readonly editor: ICodeEditor;
|
||||
private _isEnabled: boolean;
|
||||
private _useFoldingProviders: boolean;
|
||||
private _unfoldOnClickAfterEndOfLine: boolean;
|
||||
private _restoringViewState: boolean;
|
||||
|
||||
private readonly foldingDecorationProvider: FoldingDecorationProvider;
|
||||
|
||||
private foldingModel: FoldingModel | null;
|
||||
private hiddenRangeModel: HiddenRangeModel | null;
|
||||
|
||||
private rangeProvider: RangeProvider | null;
|
||||
private foldingRegionPromise: CancelablePromise<FoldingRegions | null> | null;
|
||||
|
||||
private foldingStateMemento: FoldingStateMemento | null;
|
||||
|
||||
private foldingModelPromise: Promise<FoldingModel | null> | null;
|
||||
private updateScheduler: Delayer<FoldingModel | null> | null;
|
||||
|
||||
private foldingEnabled: IContextKey<boolean>;
|
||||
private cursorChangedScheduler: RunOnceScheduler | null;
|
||||
|
||||
private readonly localToDispose = this._register(new DisposableStore());
|
||||
private mouseDownInfo: { lineNumber: number, iconClicked: boolean } | null;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService
|
||||
) {
|
||||
super();
|
||||
this.editor = editor;
|
||||
const options = this.editor.getOptions();
|
||||
this._isEnabled = options.get(EditorOption.folding);
|
||||
this._useFoldingProviders = options.get(EditorOption.foldingStrategy) !== 'indentation';
|
||||
this._unfoldOnClickAfterEndOfLine = options.get(EditorOption.unfoldOnClickAfterEndOfLine);
|
||||
this._restoringViewState = false;
|
||||
|
||||
this.foldingModel = null;
|
||||
this.hiddenRangeModel = null;
|
||||
this.rangeProvider = null;
|
||||
this.foldingRegionPromise = null;
|
||||
this.foldingStateMemento = null;
|
||||
this.foldingModelPromise = null;
|
||||
this.updateScheduler = null;
|
||||
this.cursorChangedScheduler = null;
|
||||
this.mouseDownInfo = null;
|
||||
|
||||
this.foldingDecorationProvider = new FoldingDecorationProvider(editor);
|
||||
this.foldingDecorationProvider.autoHideFoldingControls = options.get(EditorOption.showFoldingControls) === 'mouseover';
|
||||
this.foldingDecorationProvider.showFoldingHighlights = options.get(EditorOption.foldingHighlight);
|
||||
this.foldingEnabled = CONTEXT_FOLDING_ENABLED.bindTo(this.contextKeyService);
|
||||
this.foldingEnabled.set(this._isEnabled);
|
||||
|
||||
this._register(this.editor.onDidChangeModel(() => this.onModelChanged()));
|
||||
|
||||
this._register(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
|
||||
if (e.hasChanged(EditorOption.folding)) {
|
||||
this._isEnabled = this.editor.getOptions().get(EditorOption.folding);
|
||||
this.foldingEnabled.set(this._isEnabled);
|
||||
this.onModelChanged();
|
||||
}
|
||||
if (e.hasChanged(EditorOption.showFoldingControls) || e.hasChanged(EditorOption.foldingHighlight)) {
|
||||
const options = this.editor.getOptions();
|
||||
this.foldingDecorationProvider.autoHideFoldingControls = options.get(EditorOption.showFoldingControls) === 'mouseover';
|
||||
this.foldingDecorationProvider.showFoldingHighlights = options.get(EditorOption.foldingHighlight);
|
||||
this.onModelContentChanged();
|
||||
}
|
||||
if (e.hasChanged(EditorOption.foldingStrategy)) {
|
||||
this._useFoldingProviders = this.editor.getOptions().get(EditorOption.foldingStrategy) !== 'indentation';
|
||||
this.onFoldingStrategyChanged();
|
||||
}
|
||||
if (e.hasChanged(EditorOption.unfoldOnClickAfterEndOfLine)) {
|
||||
this._unfoldOnClickAfterEndOfLine = this.editor.getOptions().get(EditorOption.unfoldOnClickAfterEndOfLine);
|
||||
}
|
||||
}));
|
||||
this.onModelChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Store view state.
|
||||
*/
|
||||
public saveViewState(): FoldingStateMemento | undefined {
|
||||
let model = this.editor.getModel();
|
||||
if (!model || !this._isEnabled || model.isTooLargeForTokenization()) {
|
||||
return {};
|
||||
}
|
||||
if (this.foldingModel) { // disposed ?
|
||||
let collapsedRegions = this.foldingModel.isInitialized ? this.foldingModel.getMemento() : this.hiddenRangeModel!.getMemento();
|
||||
let provider = this.rangeProvider ? this.rangeProvider.id : undefined;
|
||||
return { collapsedRegions, lineCount: model.getLineCount(), provider };
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore view state.
|
||||
*/
|
||||
public restoreViewState(state: FoldingStateMemento): void {
|
||||
let model = this.editor.getModel();
|
||||
if (!model || !this._isEnabled || model.isTooLargeForTokenization() || !this.hiddenRangeModel) {
|
||||
return;
|
||||
}
|
||||
if (!state || !state.collapsedRegions || state.lineCount !== model.getLineCount()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.provider === ID_SYNTAX_PROVIDER || state.provider === ID_INIT_PROVIDER) {
|
||||
this.foldingStateMemento = state;
|
||||
}
|
||||
|
||||
const collapsedRegions = state.collapsedRegions;
|
||||
|
||||
// set the hidden ranges right away, before waiting for the folding model.
|
||||
if (this.hiddenRangeModel.applyMemento(collapsedRegions)) {
|
||||
const foldingModel = this.getFoldingModel();
|
||||
if (foldingModel) {
|
||||
foldingModel.then(foldingModel => {
|
||||
if (foldingModel) {
|
||||
this._restoringViewState = true;
|
||||
try {
|
||||
foldingModel.applyMemento(collapsedRegions);
|
||||
} finally {
|
||||
this._restoringViewState = false;
|
||||
}
|
||||
}
|
||||
}).then(undefined, onUnexpectedError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onModelChanged(): void {
|
||||
this.localToDispose.clear();
|
||||
|
||||
let model = this.editor.getModel();
|
||||
if (!this._isEnabled || !model || model.isTooLargeForTokenization()) {
|
||||
// huge files get no view model, so they cannot support hidden areas
|
||||
return;
|
||||
}
|
||||
|
||||
this.foldingModel = new FoldingModel(model, this.foldingDecorationProvider);
|
||||
this.localToDispose.add(this.foldingModel);
|
||||
|
||||
this.hiddenRangeModel = new HiddenRangeModel(this.foldingModel);
|
||||
this.localToDispose.add(this.hiddenRangeModel);
|
||||
this.localToDispose.add(this.hiddenRangeModel.onDidChange(hr => this.onHiddenRangesChanges(hr)));
|
||||
|
||||
this.updateScheduler = new Delayer<FoldingModel>(200);
|
||||
|
||||
this.cursorChangedScheduler = new RunOnceScheduler(() => this.revealCursor(), 200);
|
||||
this.localToDispose.add(this.cursorChangedScheduler);
|
||||
this.localToDispose.add(FoldingRangeProviderRegistry.onDidChange(() => this.onFoldingStrategyChanged()));
|
||||
this.localToDispose.add(this.editor.onDidChangeModelLanguageConfiguration(() => this.onFoldingStrategyChanged())); // covers model language changes as well
|
||||
this.localToDispose.add(this.editor.onDidChangeModelContent(() => this.onModelContentChanged()));
|
||||
this.localToDispose.add(this.editor.onDidChangeCursorPosition(() => this.onCursorPositionChanged()));
|
||||
this.localToDispose.add(this.editor.onMouseDown(e => this.onEditorMouseDown(e)));
|
||||
this.localToDispose.add(this.editor.onMouseUp(e => this.onEditorMouseUp(e)));
|
||||
this.localToDispose.add({
|
||||
dispose: () => {
|
||||
if (this.foldingRegionPromise) {
|
||||
this.foldingRegionPromise.cancel();
|
||||
this.foldingRegionPromise = null;
|
||||
}
|
||||
if (this.updateScheduler) {
|
||||
this.updateScheduler.cancel();
|
||||
}
|
||||
this.updateScheduler = null;
|
||||
this.foldingModel = null;
|
||||
this.foldingModelPromise = null;
|
||||
this.hiddenRangeModel = null;
|
||||
this.cursorChangedScheduler = null;
|
||||
this.foldingStateMemento = null;
|
||||
if (this.rangeProvider) {
|
||||
this.rangeProvider.dispose();
|
||||
}
|
||||
this.rangeProvider = null;
|
||||
}
|
||||
});
|
||||
this.onModelContentChanged();
|
||||
}
|
||||
|
||||
private onFoldingStrategyChanged() {
|
||||
if (this.rangeProvider) {
|
||||
this.rangeProvider.dispose();
|
||||
}
|
||||
this.rangeProvider = null;
|
||||
this.onModelContentChanged();
|
||||
}
|
||||
|
||||
private getRangeProvider(editorModel: ITextModel): RangeProvider {
|
||||
if (this.rangeProvider) {
|
||||
return this.rangeProvider;
|
||||
}
|
||||
this.rangeProvider = new IndentRangeProvider(editorModel); // fallback
|
||||
|
||||
|
||||
if (this._useFoldingProviders && this.foldingModel) {
|
||||
let foldingProviders = FoldingRangeProviderRegistry.ordered(this.foldingModel.textModel);
|
||||
if (foldingProviders.length === 0 && this.foldingStateMemento && this.foldingStateMemento.collapsedRegions) {
|
||||
const rangeProvider = this.rangeProvider = new InitializingRangeProvider(editorModel, this.foldingStateMemento.collapsedRegions, () => {
|
||||
// if after 30 the InitializingRangeProvider is still not replaced, force a refresh
|
||||
this.foldingStateMemento = null;
|
||||
this.onFoldingStrategyChanged();
|
||||
}, 30000);
|
||||
return rangeProvider; // keep memento in case there are still no foldingProviders on the next request.
|
||||
} else if (foldingProviders.length > 0) {
|
||||
this.rangeProvider = new SyntaxRangeProvider(editorModel, foldingProviders, () => this.onModelContentChanged());
|
||||
}
|
||||
}
|
||||
this.foldingStateMemento = null;
|
||||
return this.rangeProvider;
|
||||
}
|
||||
|
||||
public getFoldingModel() {
|
||||
return this.foldingModelPromise;
|
||||
}
|
||||
|
||||
private onModelContentChanged() {
|
||||
if (this.updateScheduler) {
|
||||
if (this.foldingRegionPromise) {
|
||||
this.foldingRegionPromise.cancel();
|
||||
this.foldingRegionPromise = null;
|
||||
}
|
||||
this.foldingModelPromise = this.updateScheduler.trigger(() => {
|
||||
const foldingModel = this.foldingModel;
|
||||
if (!foldingModel) { // null if editor has been disposed, or folding turned off
|
||||
return null;
|
||||
}
|
||||
let foldingRegionPromise = this.foldingRegionPromise = createCancelablePromise(token => this.getRangeProvider(foldingModel.textModel).compute(token));
|
||||
return foldingRegionPromise.then(foldingRanges => {
|
||||
if (foldingRanges && foldingRegionPromise === this.foldingRegionPromise) { // new request or cancelled in the meantime?
|
||||
// some cursors might have moved into hidden regions, make sure they are in expanded regions
|
||||
let selections = this.editor.getSelections();
|
||||
let selectionLineNumbers = selections ? selections.map(s => s.startLineNumber) : [];
|
||||
foldingModel.update(foldingRanges, selectionLineNumbers);
|
||||
}
|
||||
return foldingModel;
|
||||
});
|
||||
}).then(undefined, (err) => {
|
||||
onUnexpectedError(err);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private onHiddenRangesChanges(hiddenRanges: IRange[]) {
|
||||
if (this.hiddenRangeModel && hiddenRanges.length && !this._restoringViewState) {
|
||||
let selections = this.editor.getSelections();
|
||||
if (selections) {
|
||||
if (this.hiddenRangeModel.adjustSelections(selections)) {
|
||||
this.editor.setSelections(selections);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.editor.setHiddenAreas(hiddenRanges);
|
||||
}
|
||||
|
||||
private onCursorPositionChanged() {
|
||||
if (this.hiddenRangeModel && this.hiddenRangeModel.hasRanges()) {
|
||||
this.cursorChangedScheduler!.schedule();
|
||||
}
|
||||
}
|
||||
|
||||
private revealCursor() {
|
||||
const foldingModel = this.getFoldingModel();
|
||||
if (!foldingModel) {
|
||||
return;
|
||||
}
|
||||
foldingModel.then(foldingModel => { // null is returned if folding got disabled in the meantime
|
||||
if (foldingModel) {
|
||||
let selections = this.editor.getSelections();
|
||||
if (selections && selections.length > 0) {
|
||||
let toToggle: FoldingRegion[] = [];
|
||||
for (let selection of selections) {
|
||||
let lineNumber = selection.selectionStartLineNumber;
|
||||
if (this.hiddenRangeModel && this.hiddenRangeModel.isHidden(lineNumber)) {
|
||||
toToggle.push(...foldingModel.getAllRegionsAtLine(lineNumber, r => r.isCollapsed && lineNumber > r.startLineNumber));
|
||||
}
|
||||
}
|
||||
if (toToggle.length) {
|
||||
foldingModel.toggleCollapseState(toToggle);
|
||||
this.reveal(selections[0].getPosition());
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then(undefined, onUnexpectedError);
|
||||
|
||||
}
|
||||
|
||||
private onEditorMouseDown(e: IEditorMouseEvent): void {
|
||||
this.mouseDownInfo = null;
|
||||
|
||||
|
||||
if (!this.hiddenRangeModel || !e.target || !e.target.range) {
|
||||
return;
|
||||
}
|
||||
if (!e.event.leftButton && !e.event.middleButton) {
|
||||
return;
|
||||
}
|
||||
const range = e.target.range;
|
||||
let iconClicked = false;
|
||||
switch (e.target.type) {
|
||||
case MouseTargetType.GUTTER_LINE_DECORATIONS:
|
||||
const data = e.target.detail as IMarginData;
|
||||
const offsetLeftInGutter = (e.target.element as HTMLElement).offsetLeft;
|
||||
const gutterOffsetX = data.offsetX - offsetLeftInGutter;
|
||||
|
||||
// const gutterOffsetX = data.offsetX - data.glyphMarginWidth - data.lineNumbersWidth - data.glyphMarginLeft;
|
||||
|
||||
// TODO@joao TODO@alex TODO@martin this is such that we don't collide with dirty diff
|
||||
if (gutterOffsetX < 5) { // the whitespace between the border and the real folding icon border is 5px
|
||||
return;
|
||||
}
|
||||
|
||||
iconClicked = true;
|
||||
break;
|
||||
case MouseTargetType.CONTENT_EMPTY: {
|
||||
if (this._unfoldOnClickAfterEndOfLine && this.hiddenRangeModel.hasRanges()) {
|
||||
const data = e.target.detail as IEmptyContentData;
|
||||
if (!data.isAfterLines) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
case MouseTargetType.CONTENT_TEXT: {
|
||||
if (this.hiddenRangeModel.hasRanges()) {
|
||||
let model = this.editor.getModel();
|
||||
if (model && range.startColumn === model.getLineMaxColumn(range.startLineNumber)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
this.mouseDownInfo = { lineNumber: range.startLineNumber, iconClicked };
|
||||
}
|
||||
|
||||
private onEditorMouseUp(e: IEditorMouseEvent): void {
|
||||
const foldingModel = this.getFoldingModel();
|
||||
if (!foldingModel || !this.mouseDownInfo || !e.target) {
|
||||
return;
|
||||
}
|
||||
let lineNumber = this.mouseDownInfo.lineNumber;
|
||||
let iconClicked = this.mouseDownInfo.iconClicked;
|
||||
|
||||
let range = e.target.range;
|
||||
if (!range || range.startLineNumber !== lineNumber) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (iconClicked) {
|
||||
if (e.target.type !== MouseTargetType.GUTTER_LINE_DECORATIONS) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
let model = this.editor.getModel();
|
||||
if (!model || range.startColumn !== model.getLineMaxColumn(lineNumber)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foldingModel.then(foldingModel => {
|
||||
if (foldingModel) {
|
||||
let region = foldingModel.getRegionAtLine(lineNumber);
|
||||
if (region && region.startLineNumber === lineNumber) {
|
||||
let isCollapsed = region.isCollapsed;
|
||||
if (iconClicked || isCollapsed) {
|
||||
let toToggle = [];
|
||||
let recursive = e.event.middleButton || e.event.shiftKey;
|
||||
if (recursive) {
|
||||
for (const r of foldingModel.getRegionsInside(region)) {
|
||||
if (r.isCollapsed === isCollapsed) {
|
||||
toToggle.push(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
// when recursive, first only collapse all children. If all are already folded or there are no children, also fold parent.
|
||||
if (isCollapsed || !recursive || toToggle.length === 0) {
|
||||
toToggle.push(region);
|
||||
}
|
||||
foldingModel.toggleCollapseState(toToggle);
|
||||
this.reveal({ lineNumber, column: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}).then(undefined, onUnexpectedError);
|
||||
}
|
||||
|
||||
public reveal(position: IPosition): void {
|
||||
this.editor.revealPositionInCenterIfOutsideViewport(position, ScrollType.Smooth);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FoldingAction<T> extends EditorAction {
|
||||
|
||||
abstract invoke(foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: T): void;
|
||||
|
||||
public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: T): void | Promise<void> {
|
||||
let foldingController = FoldingController.get(editor);
|
||||
if (!foldingController) {
|
||||
return;
|
||||
}
|
||||
let foldingModelPromise = foldingController.getFoldingModel();
|
||||
if (foldingModelPromise) {
|
||||
this.reportTelemetry(accessor, editor);
|
||||
return foldingModelPromise.then(foldingModel => {
|
||||
if (foldingModel) {
|
||||
this.invoke(foldingController, foldingModel, editor, args);
|
||||
const selection = editor.getSelection();
|
||||
if (selection) {
|
||||
foldingController.reveal(selection.getStartPosition());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected getSelectedLines(editor: ICodeEditor) {
|
||||
let selections = editor.getSelections();
|
||||
return selections ? selections.map(s => s.startLineNumber) : [];
|
||||
}
|
||||
|
||||
protected getLineNumbers(args: FoldingArguments, editor: ICodeEditor) {
|
||||
if (args && args.selectionLines) {
|
||||
return args.selectionLines.map(l => l + 1); // to 0-bases line numbers
|
||||
}
|
||||
return this.getSelectedLines(editor);
|
||||
}
|
||||
|
||||
public run(_accessor: ServicesAccessor, _editor: ICodeEditor): void {
|
||||
}
|
||||
}
|
||||
|
||||
interface FoldingArguments {
|
||||
levels?: number;
|
||||
direction?: 'up' | 'down';
|
||||
selectionLines?: number[];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
if (!types.isUndefined(foldingArgs.selectionLines) && (!types.isArray(foldingArgs.selectionLines) || !foldingArgs.selectionLines.every(types.isNumber))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
class UnfoldAction extends FoldingAction<FoldingArguments> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.unfold',
|
||||
label: nls.localize('unfoldAction.label', "Unfold"),
|
||||
alias: 'Unfold',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_CLOSE_SQUARE_BRACKET
|
||||
},
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
description: {
|
||||
description: 'Unfold the content in the editor',
|
||||
args: [
|
||||
{
|
||||
name: 'Unfold editor argument',
|
||||
description: `Property-value pairs that can be passed through this argument:
|
||||
* 'levels': Number of levels to unfold. If not set, defaults to 1.
|
||||
* 'direction': If 'up', unfold given number of levels up otherwise unfolds down.
|
||||
* 'selectionLines': The start lines (0-based) of the editor selections to apply the unfold action to. If not set, the active selection(s) will be used.
|
||||
`,
|
||||
constraint: foldingArgumentsConstraint,
|
||||
schema: {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'levels': {
|
||||
'type': 'number',
|
||||
'default': 1
|
||||
},
|
||||
'direction': {
|
||||
'type': 'string',
|
||||
'enum': ['up', 'down'],
|
||||
'default': 'down'
|
||||
},
|
||||
'selectionLines': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'number'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: FoldingArguments): void {
|
||||
let levels = args && args.levels || 1;
|
||||
let lineNumbers = this.getLineNumbers(args, editor);
|
||||
if (args && args.direction === 'up') {
|
||||
setCollapseStateLevelsUp(foldingModel, false, levels, lineNumbers);
|
||||
} else {
|
||||
setCollapseStateLevelsDown(foldingModel, false, levels, lineNumbers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UnFoldRecursivelyAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.unfoldRecursively',
|
||||
label: nls.localize('unFoldRecursivelyAction.label', "Unfold Recursively"),
|
||||
alias: 'Unfold Recursively',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_CLOSE_SQUARE_BRACKET),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, _args: any): void {
|
||||
setCollapseStateLevelsDown(foldingModel, false, Number.MAX_VALUE, this.getSelectedLines(editor));
|
||||
}
|
||||
}
|
||||
|
||||
class FoldAction extends FoldingAction<FoldingArguments> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.fold',
|
||||
label: nls.localize('foldAction.label', "Fold"),
|
||||
alias: 'Fold',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET,
|
||||
mac: {
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.US_OPEN_SQUARE_BRACKET
|
||||
},
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
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.
|
||||
* 'direction': If 'up', folds given number of levels up otherwise folds down.
|
||||
* 'selectionLines': The start lines (0-based) of the editor selections to apply the fold action to. If not set, the active selection(s) will be used.
|
||||
If no levels or direction is set, folds the region at the locations or if already collapsed, the first uncollapsed parent instead.
|
||||
`,
|
||||
constraint: foldingArgumentsConstraint,
|
||||
schema: {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'levels': {
|
||||
'type': 'number',
|
||||
},
|
||||
'direction': {
|
||||
'type': 'string',
|
||||
'enum': ['up', 'down'],
|
||||
},
|
||||
'selectionLines': {
|
||||
'type': 'array',
|
||||
'items': {
|
||||
'type': 'number'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor, args: FoldingArguments): void {
|
||||
let lineNumbers = this.getLineNumbers(args, editor);
|
||||
|
||||
const levels = args && args.levels;
|
||||
const direction = args && args.direction;
|
||||
|
||||
if (typeof levels !== 'number' && typeof direction !== 'string') {
|
||||
// fold the region at the location or if already collapsed, the first uncollapsed parent instead.
|
||||
setCollapseStateUp(foldingModel, true, lineNumbers);
|
||||
} else {
|
||||
if (direction === 'up') {
|
||||
setCollapseStateLevelsUp(foldingModel, true, levels || 1, lineNumbers);
|
||||
} else {
|
||||
setCollapseStateLevelsDown(foldingModel, true, levels || 1, lineNumbers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ToggleFoldAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.toggleFold',
|
||||
label: nls.localize('toggleFoldAction.label', "Toggle Fold"),
|
||||
alias: 'Toggle Fold',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_L),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {
|
||||
let selectedLines = this.getSelectedLines(editor);
|
||||
toggleCollapseState(foldingModel, 1, selectedLines);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FoldRecursivelyAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.foldRecursively',
|
||||
label: nls.localize('foldRecursivelyAction.label', "Fold Recursively"),
|
||||
alias: 'Fold Recursively',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_OPEN_SQUARE_BRACKET),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {
|
||||
let selectedLines = this.getSelectedLines(editor);
|
||||
setCollapseStateLevelsDown(foldingModel, true, Number.MAX_VALUE, selectedLines);
|
||||
}
|
||||
}
|
||||
|
||||
class FoldAllBlockCommentsAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.foldAllBlockComments',
|
||||
label: nls.localize('foldAllBlockComments.label', "Fold All Block Comments"),
|
||||
alias: 'Fold All Block Comments',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.US_SLASH),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {
|
||||
if (foldingModel.regions.hasTypes()) {
|
||||
setCollapseStateForType(foldingModel, FoldingRangeKind.Comment.value, true);
|
||||
} else {
|
||||
const editorModel = editor.getModel();
|
||||
if (!editorModel) {
|
||||
return;
|
||||
}
|
||||
let comments = LanguageConfigurationRegistry.getComments(editorModel.getLanguageIdentifier().id);
|
||||
if (comments && comments.blockCommentStartToken) {
|
||||
let regExp = new RegExp('^\\s*' + escapeRegExpCharacters(comments.blockCommentStartToken));
|
||||
setCollapseStateForMatchingLines(foldingModel, regExp, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FoldAllRegionsAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.foldAllMarkerRegions',
|
||||
label: nls.localize('foldAllMarkerRegions.label', "Fold All Regions"),
|
||||
alias: 'Fold All Regions',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_8),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {
|
||||
if (foldingModel.regions.hasTypes()) {
|
||||
setCollapseStateForType(foldingModel, FoldingRangeKind.Region.value, true);
|
||||
} else {
|
||||
const editorModel = editor.getModel();
|
||||
if (!editorModel) {
|
||||
return;
|
||||
}
|
||||
let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editorModel.getLanguageIdentifier().id);
|
||||
if (foldingRules && foldingRules.markers && foldingRules.markers.start) {
|
||||
let regExp = new RegExp(foldingRules.markers.start);
|
||||
setCollapseStateForMatchingLines(foldingModel, regExp, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class UnfoldAllRegionsAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.unfoldAllMarkerRegions',
|
||||
label: nls.localize('unfoldAllMarkerRegions.label', "Unfold All Regions"),
|
||||
alias: 'Unfold All Regions',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_9),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {
|
||||
if (foldingModel.regions.hasTypes()) {
|
||||
setCollapseStateForType(foldingModel, FoldingRangeKind.Region.value, false);
|
||||
} else {
|
||||
const editorModel = editor.getModel();
|
||||
if (!editorModel) {
|
||||
return;
|
||||
}
|
||||
let foldingRules = LanguageConfigurationRegistry.getFoldingRules(editorModel.getLanguageIdentifier().id);
|
||||
if (foldingRules && foldingRules.markers && foldingRules.markers.start) {
|
||||
let regExp = new RegExp(foldingRules.markers.start);
|
||||
setCollapseStateForMatchingLines(foldingModel, regExp, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FoldAllAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.foldAll',
|
||||
label: nls.localize('foldAllAction.label', "Fold All"),
|
||||
alias: 'Fold All',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_0),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, _editor: ICodeEditor): void {
|
||||
setCollapseStateLevelsDown(foldingModel, true);
|
||||
}
|
||||
}
|
||||
|
||||
class UnfoldAllAction extends FoldingAction<void> {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.unfoldAll',
|
||||
label: nls.localize('unfoldAllAction.label', "Unfold All"),
|
||||
alias: 'Unfold All',
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_J),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, _editor: ICodeEditor): void {
|
||||
setCollapseStateLevelsDown(foldingModel, false);
|
||||
}
|
||||
}
|
||||
|
||||
class FoldLevelAction extends FoldingAction<void> {
|
||||
private static readonly ID_PREFIX = 'editor.foldLevel';
|
||||
public static readonly ID = (level: number) => FoldLevelAction.ID_PREFIX + level;
|
||||
|
||||
private getFoldingLevel() {
|
||||
return parseInt(this.id.substr(FoldLevelAction.ID_PREFIX.length));
|
||||
}
|
||||
|
||||
invoke(_foldingController: FoldingController, foldingModel: FoldingModel, editor: ICodeEditor): void {
|
||||
setCollapseStateAtLevel(foldingModel, this.getFoldingLevel(), true, this.getSelectedLines(editor));
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(FoldingController.ID, FoldingController);
|
||||
registerEditorAction(UnfoldAction);
|
||||
registerEditorAction(UnFoldRecursivelyAction);
|
||||
registerEditorAction(FoldAction);
|
||||
registerEditorAction(FoldRecursivelyAction);
|
||||
registerEditorAction(FoldAllAction);
|
||||
registerEditorAction(UnfoldAllAction);
|
||||
registerEditorAction(FoldAllBlockCommentsAction);
|
||||
registerEditorAction(FoldAllRegionsAction);
|
||||
registerEditorAction(UnfoldAllRegionsAction);
|
||||
registerEditorAction(ToggleFoldAction);
|
||||
|
||||
for (let i = 1; i <= 7; i++) {
|
||||
registerInstantiatedEditorAction(
|
||||
new FoldLevelAction({
|
||||
id: FoldLevelAction.ID(i),
|
||||
label: nls.localize('foldLevelAction.label', "Fold Level {0}", i),
|
||||
alias: `Fold Level ${i}`,
|
||||
precondition: CONTEXT_FOLDING_ENABLED,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | (KeyCode.KEY_0 + i)),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export const foldBackgroundBackground = registerColor('editor.foldBackground', { light: transparent(editorSelectionBackground, 0.3), dark: transparent(editorSelectionBackground, 0.3), hc: null }, nls.localize('foldBackgroundBackground', "Background color behind folded ranges. The color must not be opaque so as not to hide underlying decorations."), true);
|
||||
export const editorFoldForeground = registerColor('editorGutter.foldingControlForeground', { dark: iconForeground, light: iconForeground, hc: iconForeground }, nls.localize('editorGutter.foldingControlForeground', 'Color of the folding control in the editor gutter.'));
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const foldBackground = theme.getColor(foldBackgroundBackground);
|
||||
if (foldBackground) {
|
||||
collector.addRule(`.monaco-editor .folded-background { background-color: ${foldBackground}; }`);
|
||||
}
|
||||
|
||||
const editorFoldColor = theme.getColor(editorFoldForeground);
|
||||
if (editorFoldColor) {
|
||||
collector.addRule(`
|
||||
.monaco-editor .cldr${foldingExpandedIcon.cssSelector},
|
||||
.monaco-editor .cldr${foldingCollapsedIcon.cssSelector} {
|
||||
color: ${editorFoldColor} !important;
|
||||
}
|
||||
`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { TrackedRangeStickiness, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { IDecorationProvider } from 'vs/editor/contrib/folding/foldingModel';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
|
||||
export const foldingExpandedIcon = registerIcon('folding-expanded', Codicon.chevronDown);
|
||||
export const foldingCollapsedIcon = registerIcon('folding-collapsed', Codicon.chevronRight);
|
||||
|
||||
export class FoldingDecorationProvider implements IDecorationProvider {
|
||||
|
||||
private static readonly COLLAPSED_VISUAL_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
afterContentClassName: 'inline-folded',
|
||||
isWholeLine: true,
|
||||
firstLineDecorationClassName: foldingCollapsedIcon.classNames
|
||||
});
|
||||
|
||||
private static readonly COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
afterContentClassName: 'inline-folded',
|
||||
className: 'folded-background',
|
||||
isWholeLine: true,
|
||||
firstLineDecorationClassName: foldingCollapsedIcon.classNames
|
||||
});
|
||||
|
||||
private static readonly EXPANDED_AUTO_HIDE_VISUAL_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
isWholeLine: true,
|
||||
firstLineDecorationClassName: foldingExpandedIcon.classNames
|
||||
});
|
||||
|
||||
private static readonly EXPANDED_VISUAL_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
isWholeLine: true,
|
||||
firstLineDecorationClassName: 'alwaysShowFoldIcons ' + foldingExpandedIcon.classNames
|
||||
});
|
||||
|
||||
private static readonly HIDDEN_RANGE_DECORATION = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
|
||||
});
|
||||
|
||||
public autoHideFoldingControls: boolean = true;
|
||||
|
||||
public showFoldingHighlights: boolean = true;
|
||||
|
||||
constructor(private readonly editor: ICodeEditor) {
|
||||
}
|
||||
|
||||
getDecorationOption(isCollapsed: boolean, isHidden: boolean): ModelDecorationOptions {
|
||||
if (isHidden) {
|
||||
return FoldingDecorationProvider.HIDDEN_RANGE_DECORATION;
|
||||
}
|
||||
if (isCollapsed) {
|
||||
return this.showFoldingHighlights ? FoldingDecorationProvider.COLLAPSED_HIGHLIGHTED_VISUAL_DECORATION : FoldingDecorationProvider.COLLAPSED_VISUAL_DECORATION;
|
||||
} else if (this.autoHideFoldingControls) {
|
||||
return FoldingDecorationProvider.EXPANDED_AUTO_HIDE_VISUAL_DECORATION;
|
||||
} else {
|
||||
return FoldingDecorationProvider.EXPANDED_VISUAL_DECORATION;
|
||||
}
|
||||
}
|
||||
|
||||
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[] {
|
||||
return this.editor.deltaDecorations(oldDecorations, newDecorations);
|
||||
}
|
||||
|
||||
changeDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T {
|
||||
return this.editor.changeDecorations(callback);
|
||||
}
|
||||
}
|
||||
403
lib/vscode/src/vs/editor/contrib/folding/foldingModel.ts
Normal file
403
lib/vscode/src/vs/editor/contrib/folding/foldingModel.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ITextModel, IModelDecorationOptions, IModelDeltaDecoration, IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { FoldingRegions, ILineRange, FoldingRegion } from './foldingRanges';
|
||||
|
||||
export interface IDecorationProvider {
|
||||
getDecorationOption(isCollapsed: boolean, isHidden: boolean): IModelDecorationOptions;
|
||||
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[];
|
||||
changeDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): T | null;
|
||||
}
|
||||
|
||||
export interface FoldingModelChangeEvent {
|
||||
model: FoldingModel;
|
||||
collapseStateChanged?: FoldingRegion[];
|
||||
}
|
||||
|
||||
export type CollapseMemento = ILineRange[];
|
||||
|
||||
export class FoldingModel {
|
||||
private readonly _textModel: ITextModel;
|
||||
private readonly _decorationProvider: IDecorationProvider;
|
||||
|
||||
private _regions: FoldingRegions;
|
||||
private _editorDecorationIds: string[];
|
||||
private _isInitialized: boolean;
|
||||
|
||||
private readonly _updateEventEmitter = new Emitter<FoldingModelChangeEvent>();
|
||||
public readonly onDidChange: Event<FoldingModelChangeEvent> = this._updateEventEmitter.event;
|
||||
|
||||
public get regions(): FoldingRegions { return this._regions; }
|
||||
public get textModel() { return this._textModel; }
|
||||
public get isInitialized() { return this._isInitialized; }
|
||||
public get decorationProvider() { return this._decorationProvider; }
|
||||
|
||||
constructor(textModel: ITextModel, decorationProvider: IDecorationProvider) {
|
||||
this._textModel = textModel;
|
||||
this._decorationProvider = decorationProvider;
|
||||
this._regions = new FoldingRegions(new Uint32Array(0), new Uint32Array(0));
|
||||
this._editorDecorationIds = [];
|
||||
this._isInitialized = false;
|
||||
}
|
||||
|
||||
public toggleCollapseState(toggledRegions: FoldingRegion[]) {
|
||||
if (!toggledRegions.length) {
|
||||
return;
|
||||
}
|
||||
toggledRegions = toggledRegions.sort((r1, r2) => r1.regionIndex - r2.regionIndex);
|
||||
|
||||
const processed: { [key: string]: boolean | undefined } = {};
|
||||
this._decorationProvider.changeDecorations(accessor => {
|
||||
let k = 0; // index from [0 ... this.regions.length]
|
||||
let dirtyRegionEndLine = -1; // end of the range where decorations need to be updated
|
||||
let lastHiddenLine = -1; // the end of the last hidden lines
|
||||
const updateDecorationsUntil = (index: number) => {
|
||||
while (k < index) {
|
||||
const endLineNumber = this._regions.getEndLineNumber(k);
|
||||
const isCollapsed = this._regions.isCollapsed(k);
|
||||
if (endLineNumber <= dirtyRegionEndLine) {
|
||||
accessor.changeDecorationOptions(this._editorDecorationIds[k], this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine));
|
||||
}
|
||||
if (isCollapsed && endLineNumber > lastHiddenLine) {
|
||||
lastHiddenLine = endLineNumber;
|
||||
}
|
||||
k++;
|
||||
}
|
||||
};
|
||||
for (let region of toggledRegions) {
|
||||
let index = region.regionIndex;
|
||||
let editorDecorationId = this._editorDecorationIds[index];
|
||||
if (editorDecorationId && !processed[editorDecorationId]) {
|
||||
processed[editorDecorationId] = true;
|
||||
|
||||
updateDecorationsUntil(index); // update all decorations up to current index using the old dirtyRegionEndLine
|
||||
|
||||
let newCollapseState = !this._regions.isCollapsed(index);
|
||||
this._regions.setCollapsed(index, newCollapseState);
|
||||
|
||||
dirtyRegionEndLine = Math.max(dirtyRegionEndLine, this._regions.getEndLineNumber(index));
|
||||
}
|
||||
}
|
||||
updateDecorationsUntil(this._regions.length);
|
||||
});
|
||||
this._updateEventEmitter.fire({ model: this, collapseStateChanged: toggledRegions });
|
||||
}
|
||||
|
||||
public update(newRegions: FoldingRegions, blockedLineNumers: number[] = []): void {
|
||||
let newEditorDecorations: IModelDeltaDecoration[] = [];
|
||||
|
||||
let isBlocked = (startLineNumber: number, endLineNumber: number) => {
|
||||
for (let blockedLineNumber of blockedLineNumers) {
|
||||
if (startLineNumber < blockedLineNumber && blockedLineNumber <= endLineNumber) { // first line is visible
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let lastHiddenLine = -1;
|
||||
|
||||
let initRange = (index: number, isCollapsed: boolean) => {
|
||||
const startLineNumber = newRegions.getStartLineNumber(index);
|
||||
const endLineNumber = newRegions.getEndLineNumber(index);
|
||||
if (isCollapsed && isBlocked(startLineNumber, endLineNumber)) {
|
||||
isCollapsed = false;
|
||||
}
|
||||
newRegions.setCollapsed(index, isCollapsed);
|
||||
|
||||
const maxColumn = this._textModel.getLineMaxColumn(startLineNumber);
|
||||
const decorationRange = {
|
||||
startLineNumber: startLineNumber,
|
||||
startColumn: Math.max(maxColumn - 1, 1), // make it length == 1 to detect deletions
|
||||
endLineNumber: startLineNumber,
|
||||
endColumn: maxColumn
|
||||
};
|
||||
newEditorDecorations.push({ range: decorationRange, options: this._decorationProvider.getDecorationOption(isCollapsed, endLineNumber <= lastHiddenLine) });
|
||||
if (isCollapsed && endLineNumber > lastHiddenLine) {
|
||||
lastHiddenLine = endLineNumber;
|
||||
}
|
||||
};
|
||||
let i = 0;
|
||||
let nextCollapsed = () => {
|
||||
while (i < this._regions.length) {
|
||||
let isCollapsed = this._regions.isCollapsed(i);
|
||||
i++;
|
||||
if (isCollapsed) {
|
||||
return i - 1;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
let k = 0;
|
||||
let collapsedIndex = nextCollapsed();
|
||||
while (collapsedIndex !== -1 && k < newRegions.length) {
|
||||
// get the latest range
|
||||
let decRange = this._textModel.getDecorationRange(this._editorDecorationIds[collapsedIndex]);
|
||||
if (decRange) {
|
||||
let collapsedStartLineNumber = decRange.startLineNumber;
|
||||
if (decRange.startColumn === Math.max(decRange.endColumn - 1, 1) && this._textModel.getLineMaxColumn(collapsedStartLineNumber) === decRange.endColumn) { // test that the decoration is still covering the full line else it got deleted
|
||||
while (k < newRegions.length) {
|
||||
let startLineNumber = newRegions.getStartLineNumber(k);
|
||||
if (collapsedStartLineNumber >= startLineNumber) {
|
||||
initRange(k, collapsedStartLineNumber === startLineNumber);
|
||||
k++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
collapsedIndex = nextCollapsed();
|
||||
}
|
||||
while (k < newRegions.length) {
|
||||
initRange(k, false);
|
||||
k++;
|
||||
}
|
||||
|
||||
this._editorDecorationIds = this._decorationProvider.deltaDecorations(this._editorDecorationIds, newEditorDecorations);
|
||||
this._regions = newRegions;
|
||||
this._isInitialized = true;
|
||||
this._updateEventEmitter.fire({ model: this });
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse state memento, for persistence only
|
||||
*/
|
||||
public getMemento(): CollapseMemento | undefined {
|
||||
let collapsedRanges: ILineRange[] = [];
|
||||
for (let i = 0; i < this._regions.length; i++) {
|
||||
if (this._regions.isCollapsed(i)) {
|
||||
let range = this._textModel.getDecorationRange(this._editorDecorationIds[i]);
|
||||
if (range) {
|
||||
let startLineNumber = range.startLineNumber;
|
||||
let endLineNumber = range.endLineNumber + this._regions.getEndLineNumber(i) - this._regions.getStartLineNumber(i);
|
||||
collapsedRanges.push({ startLineNumber, endLineNumber });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (collapsedRanges.length > 0) {
|
||||
return collapsedRanges;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply persisted state, for persistence only
|
||||
*/
|
||||
public applyMemento(state: CollapseMemento) {
|
||||
if (!Array.isArray(state)) {
|
||||
return;
|
||||
}
|
||||
let toToogle: FoldingRegion[] = [];
|
||||
for (let range of state) {
|
||||
let region = this.getRegionAtLine(range.startLineNumber);
|
||||
if (region && !region.isCollapsed) {
|
||||
toToogle.push(region);
|
||||
}
|
||||
}
|
||||
this.toggleCollapseState(toToogle);
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
this._decorationProvider.deltaDecorations(this._editorDecorationIds, []);
|
||||
}
|
||||
|
||||
getAllRegionsAtLine(lineNumber: number, filter?: (r: FoldingRegion, level: number) => boolean): FoldingRegion[] {
|
||||
let result: FoldingRegion[] = [];
|
||||
if (this._regions) {
|
||||
let index = this._regions.findRange(lineNumber);
|
||||
let level = 1;
|
||||
while (index >= 0) {
|
||||
let current = this._regions.toRegion(index);
|
||||
if (!filter || filter(current, level)) {
|
||||
result.push(current);
|
||||
}
|
||||
level++;
|
||||
index = current.parentIndex;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
getRegionAtLine(lineNumber: number): FoldingRegion | null {
|
||||
if (this._regions) {
|
||||
let index = this._regions.findRange(lineNumber);
|
||||
if (index >= 0) {
|
||||
return this._regions.toRegion(index);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getRegionsInside(region: FoldingRegion | null, filter?: RegionFilter | RegionFilterWithLevel): FoldingRegion[] {
|
||||
let result: FoldingRegion[] = [];
|
||||
let index = region ? region.regionIndex + 1 : 0;
|
||||
let endLineNumber = region ? region.endLineNumber : Number.MAX_VALUE;
|
||||
|
||||
if (filter && filter.length === 2) {
|
||||
const levelStack: FoldingRegion[] = [];
|
||||
for (let i = index, len = this._regions.length; i < len; i++) {
|
||||
let current = this._regions.toRegion(i);
|
||||
if (this._regions.getStartLineNumber(i) < endLineNumber) {
|
||||
while (levelStack.length > 0 && !current.containedBy(levelStack[levelStack.length - 1])) {
|
||||
levelStack.pop();
|
||||
}
|
||||
levelStack.push(current);
|
||||
if (filter(current, levelStack.length)) {
|
||||
result.push(current);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (let i = index, len = this._regions.length; i < len; i++) {
|
||||
let current = this._regions.toRegion(i);
|
||||
if (this._regions.getStartLineNumber(i) < endLineNumber) {
|
||||
if (!filter || (filter as RegionFilter)(current)) {
|
||||
result.push(current);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type RegionFilter = (r: FoldingRegion) => boolean;
|
||||
type RegionFilterWithLevel = (r: FoldingRegion, level: number) => boolean;
|
||||
|
||||
|
||||
/**
|
||||
* Collapse or expand the regions at the given locations
|
||||
* @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
|
||||
* @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model.
|
||||
*/
|
||||
export function toggleCollapseState(foldingModel: FoldingModel, levels: number, lineNumbers: number[]) {
|
||||
let toToggle: FoldingRegion[] = [];
|
||||
for (let lineNumber of lineNumbers) {
|
||||
let region = foldingModel.getRegionAtLine(lineNumber);
|
||||
if (region) {
|
||||
const doCollapse = !region.isCollapsed;
|
||||
toToggle.push(region);
|
||||
if (levels > 1) {
|
||||
let regionsInside = foldingModel.getRegionsInside(region, (r, level: number) => r.isCollapsed !== doCollapse && level < levels);
|
||||
toToggle.push(...regionsInside);
|
||||
}
|
||||
}
|
||||
}
|
||||
foldingModel.toggleCollapseState(toToggle);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Collapse or expand the regions at the given locations including all children.
|
||||
* @param doCollapse Wheter to collase or expand
|
||||
* @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
|
||||
* @param lineNumbers the location of the regions to collapse or expand, or if not set, all regions in the model.
|
||||
*/
|
||||
export function setCollapseStateLevelsDown(foldingModel: FoldingModel, doCollapse: boolean, levels = Number.MAX_VALUE, lineNumbers?: number[]): void {
|
||||
let toToggle: FoldingRegion[] = [];
|
||||
if (lineNumbers && lineNumbers.length > 0) {
|
||||
for (let lineNumber of lineNumbers) {
|
||||
let region = foldingModel.getRegionAtLine(lineNumber);
|
||||
if (region) {
|
||||
if (region.isCollapsed !== doCollapse) {
|
||||
toToggle.push(region);
|
||||
}
|
||||
if (levels > 1) {
|
||||
let regionsInside = foldingModel.getRegionsInside(region, (r, level: number) => r.isCollapsed !== doCollapse && level < levels);
|
||||
toToggle.push(...regionsInside);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let regionsInside = foldingModel.getRegionsInside(null, (r, level: number) => r.isCollapsed !== doCollapse && level < levels);
|
||||
toToggle.push(...regionsInside);
|
||||
}
|
||||
foldingModel.toggleCollapseState(toToggle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse or expand the regions at the given locations including all parents.
|
||||
* @param doCollapse Wheter to collase or expand
|
||||
* @param levels The number of levels. Use 1 to only impact the regions at the location, use Number.MAX_VALUE for all levels.
|
||||
* @param lineNumbers the location of the regions to collapse or expand.
|
||||
*/
|
||||
export function setCollapseStateLevelsUp(foldingModel: FoldingModel, doCollapse: boolean, levels: number, lineNumbers: number[]): void {
|
||||
let toToggle: FoldingRegion[] = [];
|
||||
for (let lineNumber of lineNumbers) {
|
||||
let regions = foldingModel.getAllRegionsAtLine(lineNumber, (region, level) => region.isCollapsed !== doCollapse && level <= levels);
|
||||
toToggle.push(...regions);
|
||||
}
|
||||
foldingModel.toggleCollapseState(toToggle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse or expand a region at the given locations. If the inner most region is already collapsed/expanded, uses the first parent instead.
|
||||
* @param doCollapse Wheter to collase or expand
|
||||
* @param lineNumbers the location of the regions to collapse or expand.
|
||||
*/
|
||||
export function setCollapseStateUp(foldingModel: FoldingModel, doCollapse: boolean, lineNumbers: number[]): void {
|
||||
let toToggle: FoldingRegion[] = [];
|
||||
for (let lineNumber of lineNumbers) {
|
||||
let regions = foldingModel.getAllRegionsAtLine(lineNumber, (region,) => region.isCollapsed !== doCollapse);
|
||||
if (regions.length > 0) {
|
||||
toToggle.push(regions[0]);
|
||||
}
|
||||
}
|
||||
foldingModel.toggleCollapseState(toToggle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Folds or unfolds all regions that have a given level, except if they contain one of the blocked lines.
|
||||
* @param foldLevel level. Level == 1 is the top level
|
||||
* @param doCollapse Wheter to collase or expand
|
||||
*/
|
||||
export function setCollapseStateAtLevel(foldingModel: FoldingModel, foldLevel: number, doCollapse: boolean, blockedLineNumbers: number[]): void {
|
||||
let filter = (region: FoldingRegion, level: number) => level === foldLevel && region.isCollapsed !== doCollapse && !blockedLineNumbers.some(line => region.containsLine(line));
|
||||
let toToggle = foldingModel.getRegionsInside(null, filter);
|
||||
foldingModel.toggleCollapseState(toToggle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Folds all regions for which the lines start with a given regex
|
||||
* @param foldingModel the folding model
|
||||
*/
|
||||
export function setCollapseStateForMatchingLines(foldingModel: FoldingModel, regExp: RegExp, doCollapse: boolean): void {
|
||||
let editorModel = foldingModel.textModel;
|
||||
let regions = foldingModel.regions;
|
||||
let toToggle: FoldingRegion[] = [];
|
||||
for (let i = regions.length - 1; i >= 0; i--) {
|
||||
if (doCollapse !== regions.isCollapsed(i)) {
|
||||
let startLineNumber = regions.getStartLineNumber(i);
|
||||
if (regExp.test(editorModel.getLineContent(startLineNumber))) {
|
||||
toToggle.push(regions.toRegion(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
foldingModel.toggleCollapseState(toToggle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Folds all regions of the given type
|
||||
* @param foldingModel the folding model
|
||||
*/
|
||||
export function setCollapseStateForType(foldingModel: FoldingModel, type: string, doCollapse: boolean): void {
|
||||
let regions = foldingModel.regions;
|
||||
let toToggle: FoldingRegion[] = [];
|
||||
for (let i = regions.length - 1; i >= 0; i--) {
|
||||
if (doCollapse !== regions.isCollapsed(i) && type === regions.getType(i)) {
|
||||
toToggle.push(regions.toRegion(i));
|
||||
}
|
||||
}
|
||||
foldingModel.toggleCollapseState(toToggle);
|
||||
}
|
||||
210
lib/vscode/src/vs/editor/contrib/folding/foldingRanges.ts
Normal file
210
lib/vscode/src/vs/editor/contrib/folding/foldingRanges.ts
Normal file
@@ -0,0 +1,210 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export interface ILineRange {
|
||||
startLineNumber: number;
|
||||
endLineNumber: number;
|
||||
}
|
||||
|
||||
export const MAX_FOLDING_REGIONS = 0xFFFF;
|
||||
export const MAX_LINE_NUMBER = 0xFFFFFF;
|
||||
|
||||
const MASK_INDENT = 0xFF000000;
|
||||
|
||||
export class FoldingRegions {
|
||||
private readonly _startIndexes: Uint32Array;
|
||||
private readonly _endIndexes: Uint32Array;
|
||||
private readonly _collapseStates: Uint32Array;
|
||||
private _parentsComputed: boolean;
|
||||
private readonly _types: Array<string | undefined> | undefined;
|
||||
|
||||
constructor(startIndexes: Uint32Array, endIndexes: Uint32Array, types?: Array<string | undefined>) {
|
||||
if (startIndexes.length !== endIndexes.length || startIndexes.length > MAX_FOLDING_REGIONS) {
|
||||
throw new Error('invalid startIndexes or endIndexes size');
|
||||
}
|
||||
this._startIndexes = startIndexes;
|
||||
this._endIndexes = endIndexes;
|
||||
this._collapseStates = new Uint32Array(Math.ceil(startIndexes.length / 32));
|
||||
this._types = types;
|
||||
this._parentsComputed = false;
|
||||
}
|
||||
|
||||
private ensureParentIndices() {
|
||||
if (!this._parentsComputed) {
|
||||
this._parentsComputed = true;
|
||||
let parentIndexes: number[] = [];
|
||||
let isInsideLast = (startLineNumber: number, endLineNumber: number) => {
|
||||
let index = parentIndexes[parentIndexes.length - 1];
|
||||
return this.getStartLineNumber(index) <= startLineNumber && this.getEndLineNumber(index) >= endLineNumber;
|
||||
};
|
||||
for (let i = 0, len = this._startIndexes.length; i < len; i++) {
|
||||
let startLineNumber = this._startIndexes[i];
|
||||
let endLineNumber = this._endIndexes[i];
|
||||
if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) {
|
||||
throw new Error('startLineNumber or endLineNumber must not exceed ' + MAX_LINE_NUMBER);
|
||||
}
|
||||
while (parentIndexes.length > 0 && !isInsideLast(startLineNumber, endLineNumber)) {
|
||||
parentIndexes.pop();
|
||||
}
|
||||
let parentIndex = parentIndexes.length > 0 ? parentIndexes[parentIndexes.length - 1] : -1;
|
||||
parentIndexes.push(i);
|
||||
this._startIndexes[i] = startLineNumber + ((parentIndex & 0xFF) << 24);
|
||||
this._endIndexes[i] = endLineNumber + ((parentIndex & 0xFF00) << 16);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public get length(): number {
|
||||
return this._startIndexes.length;
|
||||
}
|
||||
|
||||
public getStartLineNumber(index: number): number {
|
||||
return this._startIndexes[index] & MAX_LINE_NUMBER;
|
||||
}
|
||||
|
||||
public getEndLineNumber(index: number): number {
|
||||
return this._endIndexes[index] & MAX_LINE_NUMBER;
|
||||
}
|
||||
|
||||
public getType(index: number): string | undefined {
|
||||
return this._types ? this._types[index] : undefined;
|
||||
}
|
||||
|
||||
public hasTypes() {
|
||||
return !!this._types;
|
||||
}
|
||||
|
||||
public isCollapsed(index: number): boolean {
|
||||
let arrayIndex = (index / 32) | 0;
|
||||
let bit = index % 32;
|
||||
return (this._collapseStates[arrayIndex] & (1 << bit)) !== 0;
|
||||
}
|
||||
|
||||
public setCollapsed(index: number, newState: boolean) {
|
||||
let arrayIndex = (index / 32) | 0;
|
||||
let bit = index % 32;
|
||||
let value = this._collapseStates[arrayIndex];
|
||||
if (newState) {
|
||||
this._collapseStates[arrayIndex] = value | (1 << bit);
|
||||
} else {
|
||||
this._collapseStates[arrayIndex] = value & ~(1 << bit);
|
||||
}
|
||||
}
|
||||
|
||||
public toRegion(index: number): FoldingRegion {
|
||||
return new FoldingRegion(this, index);
|
||||
}
|
||||
|
||||
public getParentIndex(index: number) {
|
||||
this.ensureParentIndices();
|
||||
let parent = ((this._startIndexes[index] & MASK_INDENT) >>> 24) + ((this._endIndexes[index] & MASK_INDENT) >>> 16);
|
||||
if (parent === MAX_FOLDING_REGIONS) {
|
||||
return -1;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
public contains(index: number, line: number) {
|
||||
return this.getStartLineNumber(index) <= line && this.getEndLineNumber(index) >= line;
|
||||
}
|
||||
|
||||
private findIndex(line: number) {
|
||||
let low = 0, high = this._startIndexes.length;
|
||||
if (high === 0) {
|
||||
return -1; // no children
|
||||
}
|
||||
while (low < high) {
|
||||
let mid = Math.floor((low + high) / 2);
|
||||
if (line < this.getStartLineNumber(mid)) {
|
||||
high = mid;
|
||||
} else {
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
return low - 1;
|
||||
}
|
||||
|
||||
public findRange(line: number): number {
|
||||
let index = this.findIndex(line);
|
||||
if (index >= 0) {
|
||||
let endLineNumber = this.getEndLineNumber(index);
|
||||
if (endLineNumber >= line) {
|
||||
return index;
|
||||
}
|
||||
index = this.getParentIndex(index);
|
||||
while (index !== -1) {
|
||||
if (this.contains(index, line)) {
|
||||
return index;
|
||||
}
|
||||
index = this.getParentIndex(index);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public toString() {
|
||||
let res: string[] = [];
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
res[i] = `[${this.isCollapsed(i) ? '+' : '-'}] ${this.getStartLineNumber(i)}/${this.getEndLineNumber(i)}`;
|
||||
}
|
||||
return res.join(', ');
|
||||
}
|
||||
|
||||
public equals(b: FoldingRegions) {
|
||||
if (this.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.length; i++) {
|
||||
if (this.getStartLineNumber(i) !== b.getStartLineNumber(i)) {
|
||||
return false;
|
||||
}
|
||||
if (this.getEndLineNumber(i) !== b.getEndLineNumber(i)) {
|
||||
return false;
|
||||
}
|
||||
if (this.getType(i) !== b.getType(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class FoldingRegion {
|
||||
|
||||
constructor(private readonly ranges: FoldingRegions, private index: number) {
|
||||
}
|
||||
|
||||
public get startLineNumber() {
|
||||
return this.ranges.getStartLineNumber(this.index);
|
||||
}
|
||||
|
||||
public get endLineNumber() {
|
||||
return this.ranges.getEndLineNumber(this.index);
|
||||
}
|
||||
|
||||
public get regionIndex() {
|
||||
return this.index;
|
||||
}
|
||||
|
||||
public get parentIndex() {
|
||||
return this.ranges.getParentIndex(this.index);
|
||||
}
|
||||
|
||||
public get isCollapsed() {
|
||||
return this.ranges.isCollapsed(this.index);
|
||||
}
|
||||
|
||||
containedBy(range: ILineRange): boolean {
|
||||
return range.startLineNumber <= this.startLineNumber && range.endLineNumber >= this.endLineNumber;
|
||||
}
|
||||
containsLine(lineNumber: number) {
|
||||
return this.startLineNumber <= lineNumber && lineNumber <= this.endLineNumber;
|
||||
}
|
||||
hidesLine(lineNumber: number) {
|
||||
return this.startLineNumber < lineNumber && lineNumber <= this.endLineNumber;
|
||||
}
|
||||
}
|
||||
157
lib/vscode/src/vs/editor/contrib/folding/hiddenRangeModel.ts
Normal file
157
lib/vscode/src/vs/editor/contrib/folding/hiddenRangeModel.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Range, IRange } from 'vs/editor/common/core/range';
|
||||
import { FoldingModel, CollapseMemento } from 'vs/editor/contrib/folding/foldingModel';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Selection } from 'vs/editor/common/core/selection';
|
||||
import { findFirstInSorted } from 'vs/base/common/arrays';
|
||||
|
||||
export class HiddenRangeModel {
|
||||
private readonly _foldingModel: FoldingModel;
|
||||
private _hiddenRanges: IRange[];
|
||||
private _foldingModelListener: IDisposable | null;
|
||||
private readonly _updateEventEmitter = new Emitter<IRange[]>();
|
||||
|
||||
public get onDidChange(): Event<IRange[]> { return this._updateEventEmitter.event; }
|
||||
public get hiddenRanges() { return this._hiddenRanges; }
|
||||
|
||||
public constructor(model: FoldingModel) {
|
||||
this._foldingModel = model;
|
||||
this._foldingModelListener = model.onDidChange(_ => this.updateHiddenRanges());
|
||||
this._hiddenRanges = [];
|
||||
if (model.regions.length) {
|
||||
this.updateHiddenRanges();
|
||||
}
|
||||
}
|
||||
|
||||
private updateHiddenRanges(): void {
|
||||
let updateHiddenAreas = false;
|
||||
let newHiddenAreas: IRange[] = [];
|
||||
let i = 0; // index into hidden
|
||||
let k = 0;
|
||||
|
||||
let lastCollapsedStart = Number.MAX_VALUE;
|
||||
let lastCollapsedEnd = -1;
|
||||
|
||||
let ranges = this._foldingModel.regions;
|
||||
for (; i < ranges.length; i++) {
|
||||
if (!ranges.isCollapsed(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let startLineNumber = ranges.getStartLineNumber(i) + 1; // the first line is not hidden
|
||||
let endLineNumber = ranges.getEndLineNumber(i);
|
||||
if (lastCollapsedStart <= startLineNumber && endLineNumber <= lastCollapsedEnd) {
|
||||
// ignore ranges contained in collapsed regions
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!updateHiddenAreas && k < this._hiddenRanges.length && this._hiddenRanges[k].startLineNumber === startLineNumber && this._hiddenRanges[k].endLineNumber === endLineNumber) {
|
||||
// reuse the old ranges
|
||||
newHiddenAreas.push(this._hiddenRanges[k]);
|
||||
k++;
|
||||
} else {
|
||||
updateHiddenAreas = true;
|
||||
newHiddenAreas.push(new Range(startLineNumber, 1, endLineNumber, 1));
|
||||
}
|
||||
lastCollapsedStart = startLineNumber;
|
||||
lastCollapsedEnd = endLineNumber;
|
||||
}
|
||||
if (updateHiddenAreas || k < this._hiddenRanges.length) {
|
||||
this.applyHiddenRanges(newHiddenAreas);
|
||||
}
|
||||
}
|
||||
|
||||
public applyMemento(state: CollapseMemento): boolean {
|
||||
if (!Array.isArray(state) || state.length === 0) {
|
||||
return false;
|
||||
}
|
||||
let hiddenRanges: IRange[] = [];
|
||||
for (let r of state) {
|
||||
if (!r.startLineNumber || !r.endLineNumber) {
|
||||
return false;
|
||||
}
|
||||
hiddenRanges.push(new Range(r.startLineNumber + 1, 1, r.endLineNumber, 1));
|
||||
}
|
||||
this.applyHiddenRanges(hiddenRanges);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapse state memento, for persistence only, only used if folding model is not yet initialized
|
||||
*/
|
||||
public getMemento(): CollapseMemento {
|
||||
return this._hiddenRanges.map(r => ({ startLineNumber: r.startLineNumber - 1, endLineNumber: r.endLineNumber }));
|
||||
}
|
||||
|
||||
private applyHiddenRanges(newHiddenAreas: IRange[]) {
|
||||
this._hiddenRanges = newHiddenAreas;
|
||||
this._updateEventEmitter.fire(newHiddenAreas);
|
||||
}
|
||||
|
||||
public hasRanges() {
|
||||
return this._hiddenRanges.length > 0;
|
||||
}
|
||||
|
||||
public isHidden(line: number): boolean {
|
||||
return findRange(this._hiddenRanges, line) !== null;
|
||||
}
|
||||
|
||||
public adjustSelections(selections: Selection[]): boolean {
|
||||
let hasChanges = false;
|
||||
let editorModel = this._foldingModel.textModel;
|
||||
let lastRange: IRange | null = null;
|
||||
|
||||
let adjustLine = (line: number) => {
|
||||
if (!lastRange || !isInside(line, lastRange)) {
|
||||
lastRange = findRange(this._hiddenRanges, line);
|
||||
}
|
||||
if (lastRange) {
|
||||
return lastRange.startLineNumber - 1;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
for (let i = 0, len = selections.length; i < len; i++) {
|
||||
let selection = selections[i];
|
||||
let adjustedStartLine = adjustLine(selection.startLineNumber);
|
||||
if (adjustedStartLine) {
|
||||
selection = selection.setStartPosition(adjustedStartLine, editorModel.getLineMaxColumn(adjustedStartLine));
|
||||
hasChanges = true;
|
||||
}
|
||||
let adjustedEndLine = adjustLine(selection.endLineNumber);
|
||||
if (adjustedEndLine) {
|
||||
selection = selection.setEndPosition(adjustedEndLine, editorModel.getLineMaxColumn(adjustedEndLine));
|
||||
hasChanges = true;
|
||||
}
|
||||
selections[i] = selection;
|
||||
}
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
|
||||
public dispose() {
|
||||
if (this.hiddenRanges.length > 0) {
|
||||
this._hiddenRanges = [];
|
||||
this._updateEventEmitter.fire(this._hiddenRanges);
|
||||
}
|
||||
if (this._foldingModelListener) {
|
||||
this._foldingModelListener.dispose();
|
||||
this._foldingModelListener = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isInside(line: number, range: IRange) {
|
||||
return line >= range.startLineNumber && line <= range.endLineNumber;
|
||||
}
|
||||
function findRange(ranges: IRange[], line: number): IRange | null {
|
||||
let i = findFirstInSorted(ranges, r => line < r.startLineNumber) - 1;
|
||||
if (i >= 0 && ranges[i].endLineNumber >= line) {
|
||||
return ranges[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
188
lib/vscode/src/vs/editor/contrib/folding/indentRangeProvider.ts
Normal file
188
lib/vscode/src/vs/editor/contrib/folding/indentRangeProvider.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { FoldingMarkers } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { FoldingRegions, MAX_LINE_NUMBER } from 'vs/editor/contrib/folding/foldingRanges';
|
||||
import { TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { RangeProvider } from './folding';
|
||||
import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
const MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT = 5000;
|
||||
|
||||
export const ID_INDENT_PROVIDER = 'indent';
|
||||
|
||||
export class IndentRangeProvider implements RangeProvider {
|
||||
readonly id = ID_INDENT_PROVIDER;
|
||||
|
||||
constructor(private readonly editorModel: ITextModel) {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
}
|
||||
|
||||
compute(cancelationToken: CancellationToken): Promise<FoldingRegions> {
|
||||
let foldingRules = LanguageConfigurationRegistry.getFoldingRules(this.editorModel.getLanguageIdentifier().id);
|
||||
let offSide = foldingRules && !!foldingRules.offSide;
|
||||
let markers = foldingRules && foldingRules.markers;
|
||||
return Promise.resolve(computeRanges(this.editorModel, offSide, markers));
|
||||
}
|
||||
}
|
||||
|
||||
// public only for testing
|
||||
export class RangesCollector {
|
||||
private readonly _startIndexes: number[];
|
||||
private readonly _endIndexes: number[];
|
||||
private readonly _indentOccurrences: number[];
|
||||
private _length: number;
|
||||
private readonly _foldingRangesLimit: number;
|
||||
|
||||
constructor(foldingRangesLimit: number) {
|
||||
this._startIndexes = [];
|
||||
this._endIndexes = [];
|
||||
this._indentOccurrences = [];
|
||||
this._length = 0;
|
||||
this._foldingRangesLimit = foldingRangesLimit;
|
||||
}
|
||||
|
||||
public insertFirst(startLineNumber: number, endLineNumber: number, indent: number) {
|
||||
if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) {
|
||||
return;
|
||||
}
|
||||
let index = this._length;
|
||||
this._startIndexes[index] = startLineNumber;
|
||||
this._endIndexes[index] = endLineNumber;
|
||||
this._length++;
|
||||
if (indent < 1000) {
|
||||
this._indentOccurrences[indent] = (this._indentOccurrences[indent] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
public toIndentRanges(model: ITextModel) {
|
||||
if (this._length <= this._foldingRangesLimit) {
|
||||
// reverse and create arrays of the exact length
|
||||
let startIndexes = new Uint32Array(this._length);
|
||||
let endIndexes = new Uint32Array(this._length);
|
||||
for (let i = this._length - 1, k = 0; i >= 0; i--, k++) {
|
||||
startIndexes[k] = this._startIndexes[i];
|
||||
endIndexes[k] = this._endIndexes[i];
|
||||
}
|
||||
return new FoldingRegions(startIndexes, endIndexes);
|
||||
} else {
|
||||
let entries = 0;
|
||||
let maxIndent = this._indentOccurrences.length;
|
||||
for (let i = 0; i < this._indentOccurrences.length; i++) {
|
||||
let n = this._indentOccurrences[i];
|
||||
if (n) {
|
||||
if (n + entries > this._foldingRangesLimit) {
|
||||
maxIndent = i;
|
||||
break;
|
||||
}
|
||||
entries += n;
|
||||
}
|
||||
}
|
||||
const tabSize = model.getOptions().tabSize;
|
||||
// reverse and create arrays of the exact length
|
||||
let startIndexes = new Uint32Array(this._foldingRangesLimit);
|
||||
let endIndexes = new Uint32Array(this._foldingRangesLimit);
|
||||
for (let i = this._length - 1, k = 0; i >= 0; i--) {
|
||||
let startIndex = this._startIndexes[i];
|
||||
let lineContent = model.getLineContent(startIndex);
|
||||
let indent = TextModel.computeIndentLevel(lineContent, tabSize);
|
||||
if (indent < maxIndent || (indent === maxIndent && entries++ < this._foldingRangesLimit)) {
|
||||
startIndexes[k] = startIndex;
|
||||
endIndexes[k] = this._endIndexes[i];
|
||||
k++;
|
||||
}
|
||||
}
|
||||
return new FoldingRegions(startIndexes, endIndexes);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface PreviousRegion {
|
||||
indent: number; // indent or -2 if a marker
|
||||
endAbove: number; // end line number for the region above
|
||||
line: number; // start line of the region. Only used for marker regions.
|
||||
}
|
||||
|
||||
export function computeRanges(model: ITextModel, offSide: boolean, markers?: FoldingMarkers, foldingRangesLimit = MAX_FOLDING_REGIONS_FOR_INDENT_LIMIT): FoldingRegions {
|
||||
const tabSize = model.getOptions().tabSize;
|
||||
let result = new RangesCollector(foldingRangesLimit);
|
||||
|
||||
let pattern: RegExp | undefined = undefined;
|
||||
if (markers) {
|
||||
pattern = new RegExp(`(${markers.start.source})|(?:${markers.end.source})`);
|
||||
}
|
||||
|
||||
let previousRegions: PreviousRegion[] = [];
|
||||
let line = model.getLineCount() + 1;
|
||||
previousRegions.push({ indent: -1, endAbove: line, line }); // sentinel, to make sure there's at least one entry
|
||||
|
||||
for (let line = model.getLineCount(); line > 0; line--) {
|
||||
let lineContent = model.getLineContent(line);
|
||||
let indent = TextModel.computeIndentLevel(lineContent, tabSize);
|
||||
let previous = previousRegions[previousRegions.length - 1];
|
||||
if (indent === -1) {
|
||||
if (offSide) {
|
||||
// for offSide languages, empty lines are associated to the previous block
|
||||
// note: the next block is already written to the results, so this only
|
||||
// impacts the end position of the block before
|
||||
previous.endAbove = line;
|
||||
}
|
||||
continue; // only whitespace
|
||||
}
|
||||
let m;
|
||||
if (pattern && (m = lineContent.match(pattern))) {
|
||||
// folding pattern match
|
||||
if (m[1]) { // start pattern match
|
||||
// discard all regions until the folding pattern
|
||||
let i = previousRegions.length - 1;
|
||||
while (i > 0 && previousRegions[i].indent !== -2) {
|
||||
i--;
|
||||
}
|
||||
if (i > 0) {
|
||||
previousRegions.length = i + 1;
|
||||
previous = previousRegions[i];
|
||||
|
||||
// new folding range from pattern, includes the end line
|
||||
result.insertFirst(line, previous.line, indent);
|
||||
previous.line = line;
|
||||
previous.indent = indent;
|
||||
previous.endAbove = line;
|
||||
continue;
|
||||
} else {
|
||||
// no end marker found, treat line as a regular line
|
||||
}
|
||||
} else { // end pattern match
|
||||
previousRegions.push({ indent: -2, endAbove: line, line });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (previous.indent > indent) {
|
||||
// discard all regions with larger indent
|
||||
do {
|
||||
previousRegions.pop();
|
||||
previous = previousRegions[previousRegions.length - 1];
|
||||
} while (previous.indent > indent);
|
||||
|
||||
// new folding range
|
||||
let endLineNumber = previous.endAbove - 1;
|
||||
if (endLineNumber - line >= 1) { // needs at east size 1
|
||||
result.insertFirst(line, endLineNumber, indent);
|
||||
}
|
||||
}
|
||||
if (previous.indent === indent) {
|
||||
previous.endAbove = line;
|
||||
} else { // previous.indent < indent
|
||||
// new region with a bigger indent
|
||||
previousRegions.push({ indent, endAbove: line, line });
|
||||
}
|
||||
}
|
||||
return result.toIndentRanges(model);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ITextModel, IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model';
|
||||
import { FoldingRegions, ILineRange } from 'vs/editor/contrib/folding/foldingRanges';
|
||||
import { RangeProvider } from './folding';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IFoldingRangeData, sanitizeRanges } from 'vs/editor/contrib/folding/syntaxRangeProvider';
|
||||
|
||||
export const ID_INIT_PROVIDER = 'init';
|
||||
|
||||
export class InitializingRangeProvider implements RangeProvider {
|
||||
readonly id = ID_INIT_PROVIDER;
|
||||
|
||||
private decorationIds: string[] | undefined;
|
||||
private timeout: any;
|
||||
|
||||
constructor(private readonly editorModel: ITextModel, initialRanges: ILineRange[], onTimeout: () => void, timeoutTime: number) {
|
||||
if (initialRanges.length) {
|
||||
let toDecorationRange = (range: ILineRange): IModelDeltaDecoration => {
|
||||
return {
|
||||
range: {
|
||||
startLineNumber: range.startLineNumber,
|
||||
startColumn: 0,
|
||||
endLineNumber: range.endLineNumber,
|
||||
endColumn: editorModel.getLineLength(range.endLineNumber)
|
||||
},
|
||||
options: {
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges
|
||||
}
|
||||
};
|
||||
};
|
||||
this.decorationIds = editorModel.deltaDecorations([], initialRanges.map(toDecorationRange));
|
||||
this.timeout = setTimeout(onTimeout, timeoutTime);
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
if (this.decorationIds) {
|
||||
this.editorModel.deltaDecorations(this.decorationIds, []);
|
||||
this.decorationIds = undefined;
|
||||
}
|
||||
if (typeof this.timeout === 'number') {
|
||||
clearTimeout(this.timeout);
|
||||
this.timeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
compute(cancelationToken: CancellationToken): Promise<FoldingRegions> {
|
||||
let foldingRangeData: IFoldingRangeData[] = [];
|
||||
if (this.decorationIds) {
|
||||
for (let id of this.decorationIds) {
|
||||
let range = this.editorModel.getDecorationRange(id);
|
||||
if (range) {
|
||||
foldingRangeData.push({ start: range.startLineNumber, end: range.endLineNumber, rank: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.resolve(sanitizeRanges(foldingRangeData, Number.MAX_VALUE));
|
||||
}
|
||||
}
|
||||
|
||||
197
lib/vscode/src/vs/editor/contrib/folding/syntaxRangeProvider.ts
Normal file
197
lib/vscode/src/vs/editor/contrib/folding/syntaxRangeProvider.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { FoldingRangeProvider, FoldingRange, FoldingContext } from 'vs/editor/common/modes';
|
||||
import { onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { RangeProvider } from './folding';
|
||||
import { MAX_LINE_NUMBER, FoldingRegions } from './foldingRanges';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
|
||||
const MAX_FOLDING_REGIONS = 5000;
|
||||
|
||||
export interface IFoldingRangeData extends FoldingRange {
|
||||
rank: number;
|
||||
}
|
||||
|
||||
const foldingContext: FoldingContext = {
|
||||
};
|
||||
|
||||
export const ID_SYNTAX_PROVIDER = 'syntax';
|
||||
|
||||
export class SyntaxRangeProvider implements RangeProvider {
|
||||
|
||||
readonly id = ID_SYNTAX_PROVIDER;
|
||||
|
||||
readonly disposables: DisposableStore | undefined;
|
||||
|
||||
constructor(private readonly editorModel: ITextModel, private providers: FoldingRangeProvider[], handleFoldingRangesChange: () => void, private limit = MAX_FOLDING_REGIONS) {
|
||||
for (const provider of providers) {
|
||||
if (typeof provider.onDidChange === 'function') {
|
||||
if (!this.disposables) {
|
||||
this.disposables = new DisposableStore();
|
||||
}
|
||||
this.disposables.add(provider.onDidChange(handleFoldingRangesChange));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
compute(cancellationToken: CancellationToken): Promise<FoldingRegions | null> {
|
||||
return collectSyntaxRanges(this.providers, this.editorModel, cancellationToken).then(ranges => {
|
||||
if (ranges) {
|
||||
let res = sanitizeRanges(ranges, this.limit);
|
||||
return res;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
function collectSyntaxRanges(providers: FoldingRangeProvider[], model: ITextModel, cancellationToken: CancellationToken): Promise<IFoldingRangeData[] | null> {
|
||||
let rangeData: IFoldingRangeData[] | null = null;
|
||||
let promises = providers.map((provider, i) => {
|
||||
return Promise.resolve(provider.provideFoldingRanges(model, foldingContext, cancellationToken)).then(ranges => {
|
||||
if (cancellationToken.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(ranges)) {
|
||||
if (!Array.isArray(rangeData)) {
|
||||
rangeData = [];
|
||||
}
|
||||
let nLines = model.getLineCount();
|
||||
for (let r of ranges) {
|
||||
if (r.start > 0 && r.end > r.start && r.end <= nLines) {
|
||||
rangeData.push({ start: r.start, end: r.end, rank: i, kind: r.kind });
|
||||
}
|
||||
}
|
||||
}
|
||||
}, onUnexpectedExternalError);
|
||||
});
|
||||
return Promise.all(promises).then(_ => {
|
||||
return rangeData;
|
||||
});
|
||||
}
|
||||
|
||||
export class RangesCollector {
|
||||
private readonly _startIndexes: number[];
|
||||
private readonly _endIndexes: number[];
|
||||
private readonly _nestingLevels: number[];
|
||||
private readonly _nestingLevelCounts: number[];
|
||||
private readonly _types: Array<string | undefined>;
|
||||
private _length: number;
|
||||
private readonly _foldingRangesLimit: number;
|
||||
|
||||
constructor(foldingRangesLimit: number) {
|
||||
this._startIndexes = [];
|
||||
this._endIndexes = [];
|
||||
this._nestingLevels = [];
|
||||
this._nestingLevelCounts = [];
|
||||
this._types = [];
|
||||
this._length = 0;
|
||||
this._foldingRangesLimit = foldingRangesLimit;
|
||||
}
|
||||
|
||||
public add(startLineNumber: number, endLineNumber: number, type: string | undefined, nestingLevel: number) {
|
||||
if (startLineNumber > MAX_LINE_NUMBER || endLineNumber > MAX_LINE_NUMBER) {
|
||||
return;
|
||||
}
|
||||
let index = this._length;
|
||||
this._startIndexes[index] = startLineNumber;
|
||||
this._endIndexes[index] = endLineNumber;
|
||||
this._nestingLevels[index] = nestingLevel;
|
||||
this._types[index] = type;
|
||||
this._length++;
|
||||
if (nestingLevel < 30) {
|
||||
this._nestingLevelCounts[nestingLevel] = (this._nestingLevelCounts[nestingLevel] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
public toIndentRanges() {
|
||||
if (this._length <= this._foldingRangesLimit) {
|
||||
let startIndexes = new Uint32Array(this._length);
|
||||
let endIndexes = new Uint32Array(this._length);
|
||||
for (let i = 0; i < this._length; i++) {
|
||||
startIndexes[i] = this._startIndexes[i];
|
||||
endIndexes[i] = this._endIndexes[i];
|
||||
}
|
||||
return new FoldingRegions(startIndexes, endIndexes, this._types);
|
||||
} else {
|
||||
let entries = 0;
|
||||
let maxLevel = this._nestingLevelCounts.length;
|
||||
for (let i = 0; i < this._nestingLevelCounts.length; i++) {
|
||||
let n = this._nestingLevelCounts[i];
|
||||
if (n) {
|
||||
if (n + entries > this._foldingRangesLimit) {
|
||||
maxLevel = i;
|
||||
break;
|
||||
}
|
||||
entries += n;
|
||||
}
|
||||
}
|
||||
|
||||
let startIndexes = new Uint32Array(this._foldingRangesLimit);
|
||||
let endIndexes = new Uint32Array(this._foldingRangesLimit);
|
||||
let types: Array<string | undefined> = [];
|
||||
for (let i = 0, k = 0; i < this._length; i++) {
|
||||
let level = this._nestingLevels[i];
|
||||
if (level < maxLevel || (level === maxLevel && entries++ < this._foldingRangesLimit)) {
|
||||
startIndexes[k] = this._startIndexes[i];
|
||||
endIndexes[k] = this._endIndexes[i];
|
||||
types[k] = this._types[i];
|
||||
k++;
|
||||
}
|
||||
}
|
||||
return new FoldingRegions(startIndexes, endIndexes, types);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function sanitizeRanges(rangeData: IFoldingRangeData[], limit: number): FoldingRegions {
|
||||
|
||||
let sorted = rangeData.sort((d1, d2) => {
|
||||
let diff = d1.start - d2.start;
|
||||
if (diff === 0) {
|
||||
diff = d1.rank - d2.rank;
|
||||
}
|
||||
return diff;
|
||||
});
|
||||
let collector = new RangesCollector(limit);
|
||||
|
||||
let top: IFoldingRangeData | undefined = undefined;
|
||||
let previous: IFoldingRangeData[] = [];
|
||||
for (let entry of sorted) {
|
||||
if (!top) {
|
||||
top = entry;
|
||||
collector.add(entry.start, entry.end, entry.kind && entry.kind.value, previous.length);
|
||||
} else {
|
||||
if (entry.start > top.start) {
|
||||
if (entry.end <= top.end) {
|
||||
previous.push(top);
|
||||
top = entry;
|
||||
collector.add(entry.start, entry.end, entry.kind && entry.kind.value, previous.length);
|
||||
} else {
|
||||
if (entry.start > top.end) {
|
||||
do {
|
||||
top = previous.pop();
|
||||
} while (top && entry.start > top.end);
|
||||
if (top) {
|
||||
previous.push(top);
|
||||
}
|
||||
top = entry;
|
||||
}
|
||||
collector.add(entry.start, entry.end, entry.kind && entry.kind.value, previous.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return collector.toIndentRanges();
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { FoldingModel, setCollapseStateAtLevel, setCollapseStateLevelsDown, setCollapseStateLevelsUp, setCollapseStateForMatchingLines, setCollapseStateUp } from 'vs/editor/contrib/folding/foldingModel';
|
||||
import { ModelDecorationOptions } from 'vs/editor/common/model/textModel';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider';
|
||||
import { TrackedRangeStickiness, IModelDeltaDecoration, ITextModel, IModelDecorationsChangeAccessor } from 'vs/editor/common/model';
|
||||
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 { FoldingRegion } from 'vs/editor/contrib/folding/foldingRanges';
|
||||
import { escapeRegExpCharacters } from 'vs/base/common/strings';
|
||||
|
||||
|
||||
interface ExpectedRegion {
|
||||
startLineNumber: number;
|
||||
endLineNumber: number;
|
||||
isCollapsed: boolean;
|
||||
}
|
||||
|
||||
interface ExpectedDecoration {
|
||||
line: number;
|
||||
type: 'hidden' | 'collapsed' | 'expanded';
|
||||
}
|
||||
|
||||
export class TestDecorationProvider {
|
||||
|
||||
private static readonly collapsedDecoration = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
linesDecorationsClassName: 'folding'
|
||||
});
|
||||
|
||||
private static readonly expandedDecoration = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
linesDecorationsClassName: 'folding'
|
||||
});
|
||||
|
||||
private static readonly hiddenDecoration = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
linesDecorationsClassName: 'folding'
|
||||
});
|
||||
|
||||
constructor(private model: ITextModel) {
|
||||
}
|
||||
|
||||
getDecorationOption(isCollapsed: boolean, isHidden: boolean): ModelDecorationOptions {
|
||||
if (isHidden) {
|
||||
return TestDecorationProvider.hiddenDecoration;
|
||||
}
|
||||
if (isCollapsed) {
|
||||
return TestDecorationProvider.collapsedDecoration;
|
||||
}
|
||||
return TestDecorationProvider.expandedDecoration;
|
||||
}
|
||||
|
||||
deltaDecorations(oldDecorations: string[], newDecorations: IModelDeltaDecoration[]): string[] {
|
||||
return this.model.deltaDecorations(oldDecorations, newDecorations);
|
||||
}
|
||||
|
||||
changeDecorations<T>(callback: (changeAccessor: IModelDecorationsChangeAccessor) => T): (T | null) {
|
||||
return this.model.changeDecorations(callback);
|
||||
}
|
||||
|
||||
getDecorations(): ExpectedDecoration[] {
|
||||
const decorations = this.model.getAllDecorations();
|
||||
const res: ExpectedDecoration[] = [];
|
||||
for (let decoration of decorations) {
|
||||
if (decoration.options === TestDecorationProvider.hiddenDecoration) {
|
||||
res.push({ line: decoration.range.startLineNumber, type: 'hidden' });
|
||||
} else if (decoration.options === TestDecorationProvider.collapsedDecoration) {
|
||||
res.push({ line: decoration.range.startLineNumber, type: 'collapsed' });
|
||||
} else if (decoration.options === TestDecorationProvider.expandedDecoration) {
|
||||
res.push({ line: decoration.range.startLineNumber, type: 'expanded' });
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
suite('Folding Model', () => {
|
||||
function r(startLineNumber: number, endLineNumber: number, isCollapsed: boolean = false): ExpectedRegion {
|
||||
return { startLineNumber, endLineNumber, isCollapsed };
|
||||
}
|
||||
|
||||
function d(line: number, type: 'hidden' | 'collapsed' | 'expanded'): ExpectedDecoration {
|
||||
return { line, type };
|
||||
}
|
||||
|
||||
function assertRegion(actual: FoldingRegion | null, expected: ExpectedRegion | null, message?: string) {
|
||||
assert.equal(!!actual, !!expected, message);
|
||||
if (actual && expected) {
|
||||
assert.equal(actual.startLineNumber, expected.startLineNumber, message);
|
||||
assert.equal(actual.endLineNumber, expected.endLineNumber, message);
|
||||
assert.equal(actual.isCollapsed, expected.isCollapsed, message);
|
||||
}
|
||||
}
|
||||
|
||||
function assertFoldedRanges(foldingModel: FoldingModel, expectedRegions: ExpectedRegion[], message?: string) {
|
||||
let actualRanges: ExpectedRegion[] = [];
|
||||
let actual = foldingModel.regions;
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
if (actual.isCollapsed(i)) {
|
||||
actualRanges.push(r(actual.getStartLineNumber(i), actual.getEndLineNumber(i)));
|
||||
}
|
||||
}
|
||||
assert.deepEqual(actualRanges, expectedRegions, message);
|
||||
}
|
||||
|
||||
function assertRanges(foldingModel: FoldingModel, expectedRegions: ExpectedRegion[], message?: string) {
|
||||
let actualRanges: ExpectedRegion[] = [];
|
||||
let actual = foldingModel.regions;
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
actualRanges.push(r(actual.getStartLineNumber(i), actual.getEndLineNumber(i), actual.isCollapsed(i)));
|
||||
}
|
||||
assert.deepEqual(actualRanges, expectedRegions, message);
|
||||
}
|
||||
|
||||
function assertDecorations(foldingModel: FoldingModel, expectedDecoration: ExpectedDecoration[], message?: string) {
|
||||
const decorationProvider = foldingModel.decorationProvider as TestDecorationProvider;
|
||||
assert.deepEqual(decorationProvider.getDecorations(), expectedDecoration, message);
|
||||
}
|
||||
|
||||
function assertRegions(actual: FoldingRegion[], expectedRegions: ExpectedRegion[], message?: string) {
|
||||
assert.deepEqual(actual.map(r => ({ startLineNumber: r.startLineNumber, endLineNumber: r.endLineNumber, isCollapsed: r.isCollapsed })), expectedRegions, message);
|
||||
}
|
||||
|
||||
test('getRegionAtLine', () => {
|
||||
let lines = [
|
||||
/* 1*/ '/**',
|
||||
/* 2*/ ' * Comment',
|
||||
/* 3*/ ' */',
|
||||
/* 4*/ 'class A {',
|
||||
/* 5*/ ' void foo() {',
|
||||
/* 6*/ ' // comment {',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, undefined);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 3, false);
|
||||
let r2 = r(4, 7, false);
|
||||
let r3 = r(5, 6, false);
|
||||
|
||||
assertRanges(foldingModel, [r1, r2, r3]);
|
||||
|
||||
assertRegion(foldingModel.getRegionAtLine(1), r1, '1');
|
||||
assertRegion(foldingModel.getRegionAtLine(2), r1, '2');
|
||||
assertRegion(foldingModel.getRegionAtLine(3), r1, '3');
|
||||
assertRegion(foldingModel.getRegionAtLine(4), r2, '4');
|
||||
assertRegion(foldingModel.getRegionAtLine(5), r3, '5');
|
||||
assertRegion(foldingModel.getRegionAtLine(6), r3, '5');
|
||||
assertRegion(foldingModel.getRegionAtLine(7), r2, '6');
|
||||
assertRegion(foldingModel.getRegionAtLine(8), null, '7');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
test('collapse', () => {
|
||||
let lines = [
|
||||
/* 1*/ '/**',
|
||||
/* 2*/ ' * Comment',
|
||||
/* 3*/ ' */',
|
||||
/* 4*/ 'class A {',
|
||||
/* 5*/ ' void foo() {',
|
||||
/* 6*/ ' // comment {',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, undefined);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 3, false);
|
||||
let r2 = r(4, 7, false);
|
||||
let r3 = r(5, 6, false);
|
||||
|
||||
assertRanges(foldingModel, [r1, r2, r3]);
|
||||
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!]);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
assertRanges(foldingModel, [r(1, 3, true), r2, r3]);
|
||||
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(5)!]);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
assertRanges(foldingModel, [r(1, 3, true), r2, r(5, 6, true)]);
|
||||
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(7)!]);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
assertRanges(foldingModel, [r(1, 3, true), r(4, 7, true), r(5, 6, true)]);
|
||||
|
||||
textModel.dispose();
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test('update', () => {
|
||||
let lines = [
|
||||
/* 1*/ '/**',
|
||||
/* 2*/ ' * Comment',
|
||||
/* 3*/ ' */',
|
||||
/* 4*/ 'class A {',
|
||||
/* 5*/ ' void foo() {',
|
||||
/* 6*/ ' // comment {',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, undefined);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 3, false);
|
||||
let r2 = r(4, 7, false);
|
||||
let r3 = r(5, 6, false);
|
||||
|
||||
assertRanges(foldingModel, [r1, r2, r3]);
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(2)!, foldingModel.getRegionAtLine(5)!]);
|
||||
|
||||
textModel.applyEdits([EditOperation.insert(new Position(4, 1), '//hello\n')]);
|
||||
|
||||
foldingModel.update(computeRanges(textModel, false, undefined));
|
||||
|
||||
assertRanges(foldingModel, [r(1, 3, true), r(5, 8, false), r(6, 7, true)]);
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('delete', () => {
|
||||
let lines = [
|
||||
/* 1*/ 'function foo() {',
|
||||
/* 2*/ ' switch (x) {',
|
||||
/* 3*/ ' case 1:',
|
||||
/* 4*/ ' //hello1',
|
||||
/* 5*/ ' break;',
|
||||
/* 6*/ ' case 2:',
|
||||
/* 7*/ ' //hello2',
|
||||
/* 8*/ ' break;',
|
||||
/* 9*/ ' case 3:',
|
||||
/* 10*/ ' //hello3',
|
||||
/* 11*/ ' break;',
|
||||
/* 12*/ ' }',
|
||||
/* 13*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, undefined);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 12, false);
|
||||
let r2 = r(2, 11, false);
|
||||
let r3 = r(3, 5, false);
|
||||
let r4 = r(6, 8, false);
|
||||
let r5 = r(9, 11, false);
|
||||
|
||||
assertRanges(foldingModel, [r1, r2, r3, r4, r5]);
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(6)!]);
|
||||
|
||||
textModel.applyEdits([EditOperation.delete(new Range(6, 11, 9, 0))]);
|
||||
|
||||
foldingModel.update(computeRanges(textModel, false, undefined));
|
||||
|
||||
assertRanges(foldingModel, [r(1, 9, false), r(2, 8, false), r(3, 5, false), r(6, 8, false)]);
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('getRegionsInside', () => {
|
||||
let lines = [
|
||||
/* 1*/ '/**',
|
||||
/* 2*/ ' * Comment',
|
||||
/* 3*/ ' */',
|
||||
/* 4*/ 'class A {',
|
||||
/* 5*/ ' void foo() {',
|
||||
/* 6*/ ' // comment {',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, undefined);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 3, false);
|
||||
let r2 = r(4, 7, false);
|
||||
let r3 = r(5, 6, false);
|
||||
|
||||
assertRanges(foldingModel, [r1, r2, r3]);
|
||||
let region1 = foldingModel.getRegionAtLine(r1.startLineNumber);
|
||||
let region2 = foldingModel.getRegionAtLine(r2.startLineNumber);
|
||||
let region3 = foldingModel.getRegionAtLine(r3.startLineNumber);
|
||||
|
||||
assertRegions(foldingModel.getRegionsInside(null), [r1, r2, r3], '1');
|
||||
assertRegions(foldingModel.getRegionsInside(region1), [], '2');
|
||||
assertRegions(foldingModel.getRegionsInside(region2), [r3], '3');
|
||||
assertRegions(foldingModel.getRegionsInside(region3), [], '4');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test('getRegionsInsideWithLevel', () => {
|
||||
let lines = [
|
||||
/* 1*/ '//#region',
|
||||
/* 2*/ '//#endregion',
|
||||
/* 3*/ 'class A {',
|
||||
/* 4*/ ' void foo() {',
|
||||
/* 5*/ ' if (true) {',
|
||||
/* 6*/ ' return;',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ ' if (true) {',
|
||||
/* 9*/ ' return;',
|
||||
/* 10*/ ' }',
|
||||
/* 11*/ ' }',
|
||||
/* 12*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, { start: /^\/\/#region$/, end: /^\/\/#endregion$/ });
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 2, false);
|
||||
let r2 = r(3, 11, false);
|
||||
let r3 = r(4, 10, false);
|
||||
let r4 = r(5, 6, false);
|
||||
let r5 = r(8, 9, false);
|
||||
|
||||
let region1 = foldingModel.getRegionAtLine(r1.startLineNumber);
|
||||
let region2 = foldingModel.getRegionAtLine(r2.startLineNumber);
|
||||
let region3 = foldingModel.getRegionAtLine(r3.startLineNumber);
|
||||
|
||||
assertRanges(foldingModel, [r1, r2, r3, r4, r5]);
|
||||
|
||||
assertRegions(foldingModel.getRegionsInside(null, (r, level) => level === 1), [r1, r2], '1');
|
||||
assertRegions(foldingModel.getRegionsInside(null, (r, level) => level === 2), [r3], '2');
|
||||
assertRegions(foldingModel.getRegionsInside(null, (r, level) => level === 3), [r4, r5], '3');
|
||||
|
||||
assertRegions(foldingModel.getRegionsInside(region2, (r, level) => level === 1), [r3], '4');
|
||||
assertRegions(foldingModel.getRegionsInside(region2, (r, level) => level === 2), [r4, r5], '5');
|
||||
assertRegions(foldingModel.getRegionsInside(region3, (r, level) => level === 1), [r4, r5], '6');
|
||||
|
||||
assertRegions(foldingModel.getRegionsInside(region2, (r, level) => r.hidesLine(9)), [r3, r5], '7');
|
||||
|
||||
assertRegions(foldingModel.getRegionsInside(region1, (r, level) => level === 1), [], '8');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test('getRegionAtLine', () => {
|
||||
let lines = [
|
||||
/* 1*/ '//#region',
|
||||
/* 2*/ 'class A {',
|
||||
/* 3*/ ' void foo() {',
|
||||
/* 4*/ ' if (true) {',
|
||||
/* 5*/ ' //hello',
|
||||
/* 6*/ ' }',
|
||||
/* 7*/ '',
|
||||
/* 8*/ ' }',
|
||||
/* 9*/ '}',
|
||||
/* 10*/ '//#endregion',
|
||||
/* 11*/ ''];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, { start: /^\/\/#region$/, end: /^\/\/#endregion$/ });
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 10, false);
|
||||
let r2 = r(2, 8, false);
|
||||
let r3 = r(3, 7, false);
|
||||
let r4 = r(4, 5, false);
|
||||
|
||||
assertRanges(foldingModel, [r1, r2, r3, r4]);
|
||||
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(1), [r1], '1');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(2), [r1, r2].reverse(), '2');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(3), [r1, r2, r3].reverse(), '3');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(4), [r1, r2, r3, r4].reverse(), '4');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(5), [r1, r2, r3, r4].reverse(), '5');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(6), [r1, r2, r3].reverse(), '6');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(7), [r1, r2, r3].reverse(), '7');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(8), [r1, r2].reverse(), '8');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(9), [r1], '9');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(10), [r1], '10');
|
||||
assertRegions(foldingModel.getAllRegionsAtLine(11), [], '10');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('setCollapseStateRecursivly', () => {
|
||||
let lines = [
|
||||
/* 1*/ '//#region',
|
||||
/* 2*/ '//#endregion',
|
||||
/* 3*/ 'class A {',
|
||||
/* 4*/ ' void foo() {',
|
||||
/* 5*/ ' if (true) {',
|
||||
/* 6*/ ' return;',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '',
|
||||
/* 9*/ ' if (true) {',
|
||||
/* 10*/ ' return;',
|
||||
/* 11*/ ' }',
|
||||
/* 12*/ ' }',
|
||||
/* 13*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, { start: /^\/\/#region$/, end: /^\/\/#endregion$/ });
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 2, false);
|
||||
let r2 = r(3, 12, false);
|
||||
let r3 = r(4, 11, false);
|
||||
let r4 = r(5, 6, false);
|
||||
let r5 = r(9, 10, false);
|
||||
assertRanges(foldingModel, [r1, r2, r3, r4, r5]);
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, true, Number.MAX_VALUE, [4]);
|
||||
assertFoldedRanges(foldingModel, [r3, r4, r5], '1');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, false, Number.MAX_VALUE, [8]);
|
||||
assertFoldedRanges(foldingModel, [], '2');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, true, Number.MAX_VALUE, [12]);
|
||||
assertFoldedRanges(foldingModel, [r2, r3, r4, r5], '1');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, false, Number.MAX_VALUE, [7]);
|
||||
assertFoldedRanges(foldingModel, [r2], '1');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, false);
|
||||
assertFoldedRanges(foldingModel, [], '1');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, true);
|
||||
assertFoldedRanges(foldingModel, [r1, r2, r3, r4, r5], '1');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test('setCollapseStateAtLevel', () => {
|
||||
let lines = [
|
||||
/* 1*/ '//#region',
|
||||
/* 2*/ '//#endregion',
|
||||
/* 3*/ 'class A {',
|
||||
/* 4*/ ' void foo() {',
|
||||
/* 5*/ ' if (true) {',
|
||||
/* 6*/ ' return;',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '',
|
||||
/* 9*/ ' if (true) {',
|
||||
/* 10*/ ' return;',
|
||||
/* 11*/ ' }',
|
||||
/* 12*/ ' }',
|
||||
/* 13*/ ' //#region',
|
||||
/* 14*/ ' const bar = 9;',
|
||||
/* 15*/ ' //#endregion',
|
||||
/* 16*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, { start: /^\s*\/\/#region$/, end: /^\s*\/\/#endregion$/ });
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 2, false);
|
||||
let r2 = r(3, 15, false);
|
||||
let r3 = r(4, 11, false);
|
||||
let r4 = r(5, 6, false);
|
||||
let r5 = r(9, 10, false);
|
||||
let r6 = r(13, 15, false);
|
||||
assertRanges(foldingModel, [r1, r2, r3, r4, r5, r6]);
|
||||
|
||||
setCollapseStateAtLevel(foldingModel, 1, true, []);
|
||||
assertFoldedRanges(foldingModel, [r1, r2], '1');
|
||||
|
||||
setCollapseStateAtLevel(foldingModel, 1, false, [5]);
|
||||
assertFoldedRanges(foldingModel, [r2], '2');
|
||||
|
||||
setCollapseStateAtLevel(foldingModel, 1, false, [1]);
|
||||
assertFoldedRanges(foldingModel, [], '3');
|
||||
|
||||
setCollapseStateAtLevel(foldingModel, 2, true, []);
|
||||
assertFoldedRanges(foldingModel, [r3, r6], '4');
|
||||
|
||||
setCollapseStateAtLevel(foldingModel, 2, false, [5, 6]);
|
||||
assertFoldedRanges(foldingModel, [r3], '5');
|
||||
|
||||
setCollapseStateAtLevel(foldingModel, 3, true, [4, 9]);
|
||||
assertFoldedRanges(foldingModel, [r3, r4], '6');
|
||||
|
||||
setCollapseStateAtLevel(foldingModel, 3, false, [4, 9]);
|
||||
assertFoldedRanges(foldingModel, [r3], '7');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('setCollapseStateLevelsDown', () => {
|
||||
let lines = [
|
||||
/* 1*/ '//#region',
|
||||
/* 2*/ '//#endregion',
|
||||
/* 3*/ 'class A {',
|
||||
/* 4*/ ' void foo() {',
|
||||
/* 5*/ ' if (true) {',
|
||||
/* 6*/ ' return;',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '',
|
||||
/* 9*/ ' if (true) {',
|
||||
/* 10*/ ' return;',
|
||||
/* 11*/ ' }',
|
||||
/* 12*/ ' }',
|
||||
/* 13*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, { start: /^\/\/#region$/, end: /^\/\/#endregion$/ });
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 2, false);
|
||||
let r2 = r(3, 12, false);
|
||||
let r3 = r(4, 11, false);
|
||||
let r4 = r(5, 6, false);
|
||||
let r5 = r(9, 10, false);
|
||||
assertRanges(foldingModel, [r1, r2, r3, r4, r5]);
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, true, 1, [4]);
|
||||
assertFoldedRanges(foldingModel, [r3], '1');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, true, 2, [4]);
|
||||
assertFoldedRanges(foldingModel, [r3, r4, r5], '2');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, false, 2, [3]);
|
||||
assertFoldedRanges(foldingModel, [r4, r5], '3');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, false, 2, [2]);
|
||||
assertFoldedRanges(foldingModel, [r4, r5], '4');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, true, 4, [2]);
|
||||
assertFoldedRanges(foldingModel, [r1, r4, r5], '5');
|
||||
|
||||
setCollapseStateLevelsDown(foldingModel, false, 4, [2, 3]);
|
||||
assertFoldedRanges(foldingModel, [], '6');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
test('setCollapseStateLevelsUp', () => {
|
||||
let lines = [
|
||||
/* 1*/ '//#region',
|
||||
/* 2*/ '//#endregion',
|
||||
/* 3*/ 'class A {',
|
||||
/* 4*/ ' void foo() {',
|
||||
/* 5*/ ' if (true) {',
|
||||
/* 6*/ ' return;',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '',
|
||||
/* 9*/ ' if (true) {',
|
||||
/* 10*/ ' return;',
|
||||
/* 11*/ ' }',
|
||||
/* 12*/ ' }',
|
||||
/* 13*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, { start: /^\/\/#region$/, end: /^\/\/#endregion$/ });
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 2, false);
|
||||
let r2 = r(3, 12, false);
|
||||
let r3 = r(4, 11, false);
|
||||
let r4 = r(5, 6, false);
|
||||
let r5 = r(9, 10, false);
|
||||
assertRanges(foldingModel, [r1, r2, r3, r4, r5]);
|
||||
|
||||
setCollapseStateLevelsUp(foldingModel, true, 1, [4]);
|
||||
assertFoldedRanges(foldingModel, [r3], '1');
|
||||
|
||||
setCollapseStateLevelsUp(foldingModel, true, 2, [4]);
|
||||
assertFoldedRanges(foldingModel, [r2, r3], '2');
|
||||
|
||||
setCollapseStateLevelsUp(foldingModel, false, 4, [1, 3, 4]);
|
||||
assertFoldedRanges(foldingModel, [], '3');
|
||||
|
||||
setCollapseStateLevelsUp(foldingModel, true, 2, [10]);
|
||||
assertFoldedRanges(foldingModel, [r3, r5], '4');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test('setCollapseStateUp', () => {
|
||||
let lines = [
|
||||
/* 1*/ '//#region',
|
||||
/* 2*/ '//#endregion',
|
||||
/* 3*/ 'class A {',
|
||||
/* 4*/ ' void foo() {',
|
||||
/* 5*/ ' if (true) {',
|
||||
/* 6*/ ' return;',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '',
|
||||
/* 9*/ ' if (true) {',
|
||||
/* 10*/ ' return;',
|
||||
/* 11*/ ' }',
|
||||
/* 12*/ ' }',
|
||||
/* 13*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, { start: /^\/\/#region$/, end: /^\/\/#endregion$/ });
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 2, false);
|
||||
let r2 = r(3, 12, false);
|
||||
let r3 = r(4, 11, false);
|
||||
let r4 = r(5, 6, false);
|
||||
let r5 = r(9, 10, false);
|
||||
assertRanges(foldingModel, [r1, r2, r3, r4, r5]);
|
||||
|
||||
setCollapseStateUp(foldingModel, true, [5]);
|
||||
assertFoldedRanges(foldingModel, [r4], '1');
|
||||
|
||||
setCollapseStateUp(foldingModel, true, [5]);
|
||||
assertFoldedRanges(foldingModel, [r3, r4], '2');
|
||||
|
||||
setCollapseStateUp(foldingModel, true, [4]);
|
||||
assertFoldedRanges(foldingModel, [r2, r3, r4], '2');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
test('setCollapseStateForMatchingLines', () => {
|
||||
let lines = [
|
||||
/* 1*/ '/**',
|
||||
/* 2*/ ' * the class',
|
||||
/* 3*/ ' */',
|
||||
/* 4*/ 'class A {',
|
||||
/* 5*/ ' /**',
|
||||
/* 6*/ ' * the foo',
|
||||
/* 7*/ ' */',
|
||||
/* 8*/ ' void foo() {',
|
||||
/* 9*/ ' /*',
|
||||
/* 10*/ ' * the comment',
|
||||
/* 11*/ ' */',
|
||||
/* 12*/ ' }',
|
||||
/* 13*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, { start: /^\/\/#region$/, end: /^\/\/#endregion$/ });
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 3, false);
|
||||
let r2 = r(4, 12, false);
|
||||
let r3 = r(5, 7, false);
|
||||
let r4 = r(8, 11, false);
|
||||
let r5 = r(9, 11, false);
|
||||
assertRanges(foldingModel, [r1, r2, r3, r4, r5]);
|
||||
|
||||
let regExp = new RegExp('^\\s*' + escapeRegExpCharacters('/*'));
|
||||
setCollapseStateForMatchingLines(foldingModel, regExp, true);
|
||||
assertFoldedRanges(foldingModel, [r1, r3, r5], '1');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test('folding decoration', () => {
|
||||
let lines = [
|
||||
/* 1*/ 'class A {',
|
||||
/* 2*/ ' void foo() {',
|
||||
/* 3*/ ' if (true) {',
|
||||
/* 4*/ ' hoo();',
|
||||
/* 5*/ ' }',
|
||||
/* 6*/ ' }',
|
||||
/* 7*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
|
||||
let ranges = computeRanges(textModel, false, undefined);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
let r1 = r(1, 6, false);
|
||||
let r2 = r(2, 5, false);
|
||||
let r3 = r(3, 4, false);
|
||||
|
||||
assertRanges(foldingModel, [r1, r2, r3]);
|
||||
assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'expanded'), d(3, 'expanded')]);
|
||||
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(2)!]);
|
||||
|
||||
assertRanges(foldingModel, [r1, r(2, 5, true), r3]);
|
||||
assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]);
|
||||
|
||||
foldingModel.update(ranges);
|
||||
|
||||
assertRanges(foldingModel, [r1, r(2, 5, true), r3]);
|
||||
assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]);
|
||||
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!]);
|
||||
|
||||
assertRanges(foldingModel, [r(1, 6, true), r(2, 5, true), r3]);
|
||||
assertDecorations(foldingModel, [d(1, 'collapsed'), d(2, 'hidden'), d(3, 'hidden')]);
|
||||
|
||||
foldingModel.update(ranges);
|
||||
|
||||
assertRanges(foldingModel, [r(1, 6, true), r(2, 5, true), r3]);
|
||||
assertDecorations(foldingModel, [d(1, 'collapsed'), d(2, 'hidden'), d(3, 'hidden')]);
|
||||
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!, foldingModel.getRegionAtLine(3)!]);
|
||||
|
||||
assertRanges(foldingModel, [r1, r(2, 5, true), r(3, 4, true)]);
|
||||
assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]);
|
||||
|
||||
foldingModel.update(ranges);
|
||||
|
||||
assertRanges(foldingModel, [r1, r(2, 5, true), r(3, 4, true)]);
|
||||
assertDecorations(foldingModel, [d(1, 'expanded'), d(2, 'collapsed'), d(3, 'hidden')]);
|
||||
|
||||
textModel.dispose();
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider';
|
||||
import { FoldingMarkers } from 'vs/editor/common/modes/languageConfiguration';
|
||||
import { MAX_FOLDING_REGIONS } from 'vs/editor/contrib/folding/foldingRanges';
|
||||
|
||||
let markers: FoldingMarkers = {
|
||||
start: /^\s*#region\b/,
|
||||
end: /^\s*#endregion\b/
|
||||
};
|
||||
|
||||
|
||||
suite('FoldingRanges', () => {
|
||||
|
||||
test('test max folding regions', () => {
|
||||
let lines: string[] = [];
|
||||
let nRegions = MAX_FOLDING_REGIONS;
|
||||
for (let i = 0; i < nRegions; i++) {
|
||||
lines.push('#region');
|
||||
}
|
||||
for (let i = 0; i < nRegions; i++) {
|
||||
lines.push('#endregion');
|
||||
}
|
||||
let model = createTextModel(lines.join('\n'));
|
||||
let actual = computeRanges(model, false, markers, MAX_FOLDING_REGIONS);
|
||||
assert.equal(actual.length, nRegions, 'len');
|
||||
for (let i = 0; i < nRegions; i++) {
|
||||
assert.equal(actual.getStartLineNumber(i), i + 1, 'start' + i);
|
||||
assert.equal(actual.getEndLineNumber(i), nRegions * 2 - i, 'end' + i);
|
||||
assert.equal(actual.getParentIndex(i), i - 1, 'parent' + i);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
test('findRange', () => {
|
||||
let lines = [
|
||||
/* 1*/ '#region',
|
||||
/* 2*/ '#endregion',
|
||||
/* 3*/ 'class A {',
|
||||
/* 4*/ ' void foo() {',
|
||||
/* 5*/ ' if (true) {',
|
||||
/* 6*/ ' return;',
|
||||
/* 7*/ ' }',
|
||||
/* 8*/ '',
|
||||
/* 9*/ ' if (true) {',
|
||||
/* 10*/ ' return;',
|
||||
/* 11*/ ' }',
|
||||
/* 12*/ ' }',
|
||||
/* 13*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
try {
|
||||
let actual = computeRanges(textModel, false, markers);
|
||||
// let r0 = r(1, 2);
|
||||
// let r1 = r(3, 12);
|
||||
// let r2 = r(4, 11);
|
||||
// let r3 = r(5, 6);
|
||||
// let r4 = r(9, 10);
|
||||
|
||||
assert.equal(actual.findRange(1), 0, '1');
|
||||
assert.equal(actual.findRange(2), 0, '2');
|
||||
assert.equal(actual.findRange(3), 1, '3');
|
||||
assert.equal(actual.findRange(4), 2, '4');
|
||||
assert.equal(actual.findRange(5), 3, '5');
|
||||
assert.equal(actual.findRange(6), 3, '6');
|
||||
assert.equal(actual.findRange(7), 2, '7');
|
||||
assert.equal(actual.findRange(8), 2, '8');
|
||||
assert.equal(actual.findRange(9), 4, '9');
|
||||
assert.equal(actual.findRange(10), 4, '10');
|
||||
assert.equal(actual.findRange(11), 2, '11');
|
||||
assert.equal(actual.findRange(12), 1, '12');
|
||||
assert.equal(actual.findRange(13), -1, '13');
|
||||
} finally {
|
||||
textModel.dispose();
|
||||
}
|
||||
|
||||
|
||||
});
|
||||
|
||||
test('setCollapsed', () => {
|
||||
let lines: string[] = [];
|
||||
let nRegions = 500;
|
||||
for (let i = 0; i < nRegions; i++) {
|
||||
lines.push('#region');
|
||||
}
|
||||
for (let i = 0; i < nRegions; i++) {
|
||||
lines.push('#endregion');
|
||||
}
|
||||
let model = createTextModel(lines.join('\n'));
|
||||
let actual = computeRanges(model, false, markers, MAX_FOLDING_REGIONS);
|
||||
assert.equal(actual.length, nRegions, 'len');
|
||||
for (let i = 0; i < nRegions; i++) {
|
||||
actual.setCollapsed(i, i % 3 === 0);
|
||||
}
|
||||
for (let i = 0; i < nRegions; i++) {
|
||||
assert.equal(actual.isCollapsed(i), i % 3 === 0, 'line' + i);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { FoldingModel } from 'vs/editor/contrib/folding/foldingModel';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider';
|
||||
import { TestDecorationProvider } from './foldingModel.test';
|
||||
import { HiddenRangeModel } from 'vs/editor/contrib/folding/hiddenRangeModel';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
|
||||
|
||||
interface ExpectedRange {
|
||||
startLineNumber: number;
|
||||
endLineNumber: number;
|
||||
}
|
||||
|
||||
suite('Hidden Range Model', () => {
|
||||
function r(startLineNumber: number, endLineNumber: number): ExpectedRange {
|
||||
return { startLineNumber, endLineNumber };
|
||||
}
|
||||
|
||||
function assertRanges(actual: IRange[], expectedRegions: ExpectedRange[], message?: string) {
|
||||
assert.deepEqual(actual.map(r => ({ startLineNumber: r.startLineNumber, endLineNumber: r.endLineNumber })), expectedRegions, message);
|
||||
}
|
||||
|
||||
test('hasRanges', () => {
|
||||
let lines = [
|
||||
/* 1*/ '/**',
|
||||
/* 2*/ ' * Comment',
|
||||
/* 3*/ ' */',
|
||||
/* 4*/ 'class A {',
|
||||
/* 5*/ ' void foo() {',
|
||||
/* 6*/ ' if (true) {',
|
||||
/* 7*/ ' //hello',
|
||||
/* 8*/ ' }',
|
||||
/* 9*/ ' }',
|
||||
/* 10*/ '}'];
|
||||
|
||||
let textModel = createTextModel(lines.join('\n'));
|
||||
let foldingModel = new FoldingModel(textModel, new TestDecorationProvider(textModel));
|
||||
let hiddenRangeModel = new HiddenRangeModel(foldingModel);
|
||||
|
||||
assert.equal(hiddenRangeModel.hasRanges(), false);
|
||||
|
||||
let ranges = computeRanges(textModel, false, undefined);
|
||||
foldingModel.update(ranges);
|
||||
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!, foldingModel.getRegionAtLine(6)!]);
|
||||
assertRanges(hiddenRangeModel.hiddenRanges, [r(2, 3), r(7, 7)]);
|
||||
|
||||
assert.equal(hiddenRangeModel.hasRanges(), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(1), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(2), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(3), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(4), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(5), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(6), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(7), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(8), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(9), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(10), false);
|
||||
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(4)!]);
|
||||
assertRanges(hiddenRangeModel.hiddenRanges, [r(2, 3), r(5, 9)]);
|
||||
|
||||
assert.equal(hiddenRangeModel.hasRanges(), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(1), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(2), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(3), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(4), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(5), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(6), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(7), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(8), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(9), true);
|
||||
assert.equal(hiddenRangeModel.isHidden(10), false);
|
||||
|
||||
foldingModel.toggleCollapseState([foldingModel.getRegionAtLine(1)!, foldingModel.getRegionAtLine(6)!, foldingModel.getRegionAtLine(4)!]);
|
||||
assertRanges(hiddenRangeModel.hiddenRanges, []);
|
||||
assert.equal(hiddenRangeModel.hasRanges(), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(1), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(2), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(3), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(4), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(5), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(6), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(7), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(8), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(9), false);
|
||||
assert.equal(hiddenRangeModel.isHidden(10), false);
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
|
||||
interface IndentRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
suite('Indentation Folding', () => {
|
||||
function r(start: number, end: number): IndentRange {
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
test('Limit by indent', () => {
|
||||
|
||||
|
||||
let lines = [
|
||||
/* 1*/ 'A',
|
||||
/* 2*/ ' A',
|
||||
/* 3*/ ' A',
|
||||
/* 4*/ ' A',
|
||||
/* 5*/ ' A',
|
||||
/* 6*/ ' A',
|
||||
/* 7*/ ' A',
|
||||
/* 8*/ ' A',
|
||||
/* 9*/ ' A',
|
||||
/* 10*/ ' A',
|
||||
/* 11*/ ' A',
|
||||
/* 12*/ ' A',
|
||||
/* 13*/ ' A',
|
||||
/* 14*/ ' A',
|
||||
/* 15*/ 'A',
|
||||
/* 16*/ ' A'
|
||||
];
|
||||
let r1 = r(1, 14); //0
|
||||
let r2 = r(3, 11); //1
|
||||
let r3 = r(4, 5); //2
|
||||
let r4 = r(6, 11); //2
|
||||
let r5 = r(8, 9); //3
|
||||
let r6 = r(10, 11); //3
|
||||
let r7 = r(12, 14); //1
|
||||
let r8 = r(13, 14);//4
|
||||
let r9 = r(15, 16);//0
|
||||
|
||||
let model = createTextModel(lines.join('\n'));
|
||||
|
||||
function assertLimit(maxEntries: number, expectedRanges: IndentRange[], message: string) {
|
||||
let indentRanges = computeRanges(model, true, undefined, maxEntries);
|
||||
assert.ok(indentRanges.length <= maxEntries, 'max ' + message);
|
||||
let actual: IndentRange[] = [];
|
||||
for (let i = 0; i < indentRanges.length; i++) {
|
||||
actual.push({ start: indentRanges.getStartLineNumber(i), end: indentRanges.getEndLineNumber(i) });
|
||||
}
|
||||
assert.deepEqual(actual, expectedRanges, message);
|
||||
}
|
||||
|
||||
assertLimit(1000, [r1, r2, r3, r4, r5, r6, r7, r8, r9], '1000');
|
||||
assertLimit(9, [r1, r2, r3, r4, r5, r6, r7, r8, r9], '9');
|
||||
assertLimit(8, [r1, r2, r3, r4, r5, r6, r7, r9], '8');
|
||||
assertLimit(7, [r1, r2, r3, r4, r5, r7, r9], '7');
|
||||
assertLimit(6, [r1, r2, r3, r4, r7, r9], '6');
|
||||
assertLimit(5, [r1, r2, r3, r7, r9], '5');
|
||||
assertLimit(4, [r1, r2, r7, r9], '4');
|
||||
assertLimit(3, [r1, r2, r9], '3');
|
||||
assertLimit(2, [r1, r9], '2');
|
||||
assertLimit(1, [r1], '1');
|
||||
assertLimit(0, [], '0');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,332 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { computeRanges } from 'vs/editor/contrib/folding/indentRangeProvider';
|
||||
import { FoldingMarkers } from 'vs/editor/common/modes/languageConfiguration';
|
||||
|
||||
interface ExpectedIndentRange {
|
||||
startLineNumber: number;
|
||||
endLineNumber: number;
|
||||
parentIndex: number;
|
||||
}
|
||||
|
||||
function assertRanges(lines: string[], expected: ExpectedIndentRange[], offside: boolean, markers?: FoldingMarkers): void {
|
||||
let model = createTextModel(lines.join('\n'));
|
||||
let actual = computeRanges(model, offside, markers);
|
||||
|
||||
let actualRanges: ExpectedIndentRange[] = [];
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
actualRanges[i] = r(actual.getStartLineNumber(i), actual.getEndLineNumber(i), actual.getParentIndex(i));
|
||||
}
|
||||
assert.deepEqual(actualRanges, expected);
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
function r(startLineNumber: number, endLineNumber: number, parentIndex: number, marker = false): ExpectedIndentRange {
|
||||
return { startLineNumber, endLineNumber, parentIndex };
|
||||
}
|
||||
|
||||
suite('Indentation Folding', () => {
|
||||
test('Fold one level', () => {
|
||||
let range = [
|
||||
'A',
|
||||
' A',
|
||||
' A',
|
||||
' A'
|
||||
];
|
||||
assertRanges(range, [r(1, 4, -1)], true);
|
||||
assertRanges(range, [r(1, 4, -1)], false);
|
||||
});
|
||||
|
||||
test('Fold two levels', () => {
|
||||
let range = [
|
||||
'A',
|
||||
' A',
|
||||
' A',
|
||||
' A',
|
||||
' A'
|
||||
];
|
||||
assertRanges(range, [r(1, 5, -1), r(3, 5, 0)], true);
|
||||
assertRanges(range, [r(1, 5, -1), r(3, 5, 0)], false);
|
||||
});
|
||||
|
||||
test('Fold three levels', () => {
|
||||
let range = [
|
||||
'A',
|
||||
' A',
|
||||
' A',
|
||||
' A',
|
||||
'A'
|
||||
];
|
||||
assertRanges(range, [r(1, 4, -1), r(2, 4, 0), r(3, 4, 1)], true);
|
||||
assertRanges(range, [r(1, 4, -1), r(2, 4, 0), r(3, 4, 1)], false);
|
||||
});
|
||||
|
||||
test('Fold decreasing indent', () => {
|
||||
let range = [
|
||||
' A',
|
||||
' A',
|
||||
'A'
|
||||
];
|
||||
assertRanges(range, [], true);
|
||||
assertRanges(range, [], false);
|
||||
});
|
||||
|
||||
test('Fold Java', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'class A {',
|
||||
/* 2*/ ' void foo() {',
|
||||
/* 3*/ ' console.log();',
|
||||
/* 4*/ ' console.log();',
|
||||
/* 5*/ ' }',
|
||||
/* 6*/ '',
|
||||
/* 7*/ ' void bar() {',
|
||||
/* 8*/ ' console.log();',
|
||||
/* 9*/ ' }',
|
||||
/*10*/ '}',
|
||||
/*11*/ 'interface B {',
|
||||
/*12*/ ' void bar();',
|
||||
/*13*/ '}',
|
||||
], [r(1, 9, -1), r(2, 4, 0), r(7, 8, 0), r(11, 12, -1)], false);
|
||||
});
|
||||
|
||||
test('Fold Javadoc', () => {
|
||||
assertRanges([
|
||||
/* 1*/ '/**',
|
||||
/* 2*/ ' * Comment',
|
||||
/* 3*/ ' */',
|
||||
/* 4*/ 'class A {',
|
||||
/* 5*/ ' void foo() {',
|
||||
/* 6*/ ' }',
|
||||
/* 7*/ '}',
|
||||
], [r(1, 3, -1), r(4, 6, -1)], false);
|
||||
});
|
||||
test('Fold Whitespace Java', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'class A {',
|
||||
/* 2*/ '',
|
||||
/* 3*/ ' void foo() {',
|
||||
/* 4*/ ' ',
|
||||
/* 5*/ ' return 0;',
|
||||
/* 6*/ ' }',
|
||||
/* 7*/ ' ',
|
||||
/* 8*/ '}',
|
||||
], [r(1, 7, -1), r(3, 5, 0)], false);
|
||||
});
|
||||
|
||||
test('Fold Whitespace Python', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'def a:',
|
||||
/* 2*/ ' pass',
|
||||
/* 3*/ ' ',
|
||||
/* 4*/ ' def b:',
|
||||
/* 5*/ ' pass',
|
||||
/* 6*/ ' ',
|
||||
/* 7*/ ' ',
|
||||
/* 8*/ 'def c: # since there was a deintent here'
|
||||
], [r(1, 5, -1), r(4, 5, 0)], true);
|
||||
});
|
||||
|
||||
test('Fold Tabs', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'class A {',
|
||||
/* 2*/ '\t\t',
|
||||
/* 3*/ '\tvoid foo() {',
|
||||
/* 4*/ '\t \t//hello',
|
||||
/* 5*/ '\t return 0;',
|
||||
/* 6*/ ' \t}',
|
||||
/* 7*/ ' ',
|
||||
/* 8*/ '}',
|
||||
], [r(1, 7, -1), r(3, 5, 0)], false);
|
||||
});
|
||||
});
|
||||
|
||||
let markers: FoldingMarkers = {
|
||||
start: /^\s*#region\b/,
|
||||
end: /^\s*#endregion\b/
|
||||
};
|
||||
|
||||
suite('Folding with regions', () => {
|
||||
test('Inside region, indented', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'class A {',
|
||||
/* 2*/ ' #region',
|
||||
/* 3*/ ' void foo() {',
|
||||
/* 4*/ ' ',
|
||||
/* 5*/ ' return 0;',
|
||||
/* 6*/ ' }',
|
||||
/* 7*/ ' #endregion',
|
||||
/* 8*/ '}',
|
||||
], [r(1, 7, -1), r(2, 7, 0, true), r(3, 5, 1)], false, markers);
|
||||
});
|
||||
test('Inside region, not indented', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'var x;',
|
||||
/* 2*/ '#region',
|
||||
/* 3*/ 'void foo() {',
|
||||
/* 4*/ ' ',
|
||||
/* 5*/ ' return 0;',
|
||||
/* 6*/ ' }',
|
||||
/* 7*/ '#endregion',
|
||||
/* 8*/ '',
|
||||
], [r(2, 7, -1, true), r(3, 6, 0)], false, markers);
|
||||
});
|
||||
test('Empty Regions', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'var x;',
|
||||
/* 2*/ '#region',
|
||||
/* 3*/ '#endregion',
|
||||
/* 4*/ '#region',
|
||||
/* 5*/ '',
|
||||
/* 6*/ '#endregion',
|
||||
/* 7*/ 'var y;',
|
||||
], [r(2, 3, -1, true), r(4, 6, -1, true)], false, markers);
|
||||
});
|
||||
test('Nested Regions', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'var x;',
|
||||
/* 2*/ '#region',
|
||||
/* 3*/ '#region',
|
||||
/* 4*/ '',
|
||||
/* 5*/ '#endregion',
|
||||
/* 6*/ '#endregion',
|
||||
/* 7*/ 'var y;',
|
||||
], [r(2, 6, -1, true), r(3, 5, 0, true)], false, markers);
|
||||
});
|
||||
test('Nested Regions 2', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'class A {',
|
||||
/* 2*/ ' #region',
|
||||
/* 3*/ '',
|
||||
/* 4*/ ' #region',
|
||||
/* 5*/ '',
|
||||
/* 6*/ ' #endregion',
|
||||
/* 7*/ ' // comment',
|
||||
/* 8*/ ' #endregion',
|
||||
/* 9*/ '}',
|
||||
], [r(1, 8, -1), r(2, 8, 0, true), r(4, 6, 1, true)], false, markers);
|
||||
});
|
||||
test('Incomplete Regions', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'class A {',
|
||||
/* 2*/ '#region',
|
||||
/* 3*/ ' // comment',
|
||||
/* 4*/ '}',
|
||||
], [r(2, 3, -1)], false, markers);
|
||||
});
|
||||
test('Incomplete Regions 2', () => {
|
||||
assertRanges([
|
||||
/* 1*/ '',
|
||||
/* 2*/ '#region',
|
||||
/* 3*/ '#region',
|
||||
/* 4*/ '#region',
|
||||
/* 5*/ ' // comment',
|
||||
/* 6*/ '#endregion',
|
||||
/* 7*/ '#endregion',
|
||||
/* 8*/ ' // hello',
|
||||
], [r(3, 7, -1, true), r(4, 6, 0, true)], false, markers);
|
||||
});
|
||||
test('Indented region before', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'if (x)',
|
||||
/* 2*/ ' return;',
|
||||
/* 3*/ '',
|
||||
/* 4*/ '#region',
|
||||
/* 5*/ ' // comment',
|
||||
/* 6*/ '#endregion',
|
||||
], [r(1, 3, -1), r(4, 6, -1, true)], false, markers);
|
||||
});
|
||||
test('Indented region before 2', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'if (x)',
|
||||
/* 2*/ ' log();',
|
||||
/* 3*/ '',
|
||||
/* 4*/ ' #region',
|
||||
/* 5*/ ' // comment',
|
||||
/* 6*/ ' #endregion',
|
||||
], [r(1, 6, -1), r(2, 6, 0), r(4, 6, 1, true)], false, markers);
|
||||
});
|
||||
test('Indented region in-between', () => {
|
||||
assertRanges([
|
||||
/* 1*/ '#region',
|
||||
/* 2*/ ' // comment',
|
||||
/* 3*/ ' if (x)',
|
||||
/* 4*/ ' return;',
|
||||
/* 5*/ '',
|
||||
/* 6*/ '#endregion',
|
||||
], [r(1, 6, -1, true), r(3, 5, 0)], false, markers);
|
||||
});
|
||||
test('Indented region after', () => {
|
||||
assertRanges([
|
||||
/* 1*/ '#region',
|
||||
/* 2*/ ' // comment',
|
||||
/* 3*/ '',
|
||||
/* 4*/ '#endregion',
|
||||
/* 5*/ ' if (x)',
|
||||
/* 6*/ ' return;',
|
||||
], [r(1, 4, -1, true), r(5, 6, -1)], false, markers);
|
||||
});
|
||||
test('With off-side', () => {
|
||||
assertRanges([
|
||||
/* 1*/ '#region',
|
||||
/* 2*/ ' ',
|
||||
/* 3*/ '',
|
||||
/* 4*/ '#endregion',
|
||||
/* 5*/ '',
|
||||
], [r(1, 4, -1, true)], true, markers);
|
||||
});
|
||||
test('Nested with off-side', () => {
|
||||
assertRanges([
|
||||
/* 1*/ '#region',
|
||||
/* 2*/ ' ',
|
||||
/* 3*/ '#region',
|
||||
/* 4*/ '',
|
||||
/* 5*/ '#endregion',
|
||||
/* 6*/ '',
|
||||
/* 7*/ '#endregion',
|
||||
/* 8*/ '',
|
||||
], [r(1, 7, -1, true), r(3, 5, 0, true)], true, markers);
|
||||
});
|
||||
test('Issue 35981', () => {
|
||||
assertRanges([
|
||||
/* 1*/ 'function thisFoldsToEndOfPage() {',
|
||||
/* 2*/ ' const variable = []',
|
||||
/* 3*/ ' // #region',
|
||||
/* 4*/ ' .reduce((a, b) => a,[]);',
|
||||
/* 5*/ '}',
|
||||
/* 6*/ '',
|
||||
/* 7*/ 'function thisFoldsProperly() {',
|
||||
/* 8*/ ' const foo = "bar"',
|
||||
/* 9*/ '}',
|
||||
], [r(1, 4, -1), r(2, 4, 0), r(7, 8, -1)], false, markers);
|
||||
});
|
||||
test('Misspelled Markers', () => {
|
||||
assertRanges([
|
||||
/* 1*/ '#Region',
|
||||
/* 2*/ '#endregion',
|
||||
/* 3*/ '#regionsandmore',
|
||||
/* 4*/ '#endregion',
|
||||
/* 5*/ '#region',
|
||||
/* 6*/ '#end region',
|
||||
/* 7*/ '#region',
|
||||
/* 8*/ '#endregionff',
|
||||
], [], true, markers);
|
||||
});
|
||||
test('Issue 79359', () => {
|
||||
assertRanges([
|
||||
/* 1*/ '#region',
|
||||
/* 2*/ '',
|
||||
/* 3*/ 'class A',
|
||||
/* 4*/ ' foo',
|
||||
/* 5*/ '',
|
||||
/* 6*/ 'class A',
|
||||
/* 7*/ ' foo',
|
||||
/* 8*/ '',
|
||||
/* 9*/ '#endregion',
|
||||
], [r(1, 9, -1, true), r(3, 4, 0), r(6, 7, 0)], true, markers);
|
||||
});
|
||||
});
|
||||
100
lib/vscode/src/vs/editor/contrib/folding/test/syntaxFold.test.ts
Normal file
100
lib/vscode/src/vs/editor/contrib/folding/test/syntaxFold.test.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as assert from 'assert';
|
||||
import { createTextModel } from 'vs/editor/test/common/editorTestUtils';
|
||||
import { SyntaxRangeProvider } from 'vs/editor/contrib/folding/syntaxRangeProvider';
|
||||
import { FoldingRangeProvider, FoldingRange, FoldingContext, ProviderResult } from 'vs/editor/common/modes';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface IndentRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
class TestFoldingRangeProvider implements FoldingRangeProvider {
|
||||
constructor(private model: ITextModel, private ranges: IndentRange[]) {
|
||||
}
|
||||
|
||||
provideFoldingRanges(model: ITextModel, context: FoldingContext, token: CancellationToken): ProviderResult<FoldingRange[]> {
|
||||
if (model === this.model) {
|
||||
return this.ranges;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
suite('Syntax folding', () => {
|
||||
function r(start: number, end: number): IndentRange {
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
test('Limit by nesting level', async () => {
|
||||
let lines = [
|
||||
/* 1*/ '{',
|
||||
/* 2*/ ' A',
|
||||
/* 3*/ ' {',
|
||||
/* 4*/ ' {',
|
||||
/* 5*/ ' B',
|
||||
/* 6*/ ' }',
|
||||
/* 7*/ ' {',
|
||||
/* 8*/ ' A',
|
||||
/* 9*/ ' {',
|
||||
/* 10*/ ' A',
|
||||
/* 11*/ ' }',
|
||||
/* 12*/ ' {',
|
||||
/* 13*/ ' {',
|
||||
/* 14*/ ' {',
|
||||
/* 15*/ ' A',
|
||||
/* 16*/ ' }',
|
||||
/* 17*/ ' }',
|
||||
/* 18*/ ' }',
|
||||
/* 19*/ ' }',
|
||||
/* 20*/ ' }',
|
||||
/* 21*/ '}',
|
||||
/* 22*/ '{',
|
||||
/* 23*/ ' A',
|
||||
/* 24*/ '}',
|
||||
];
|
||||
|
||||
let r1 = r(1, 20); //0
|
||||
let r2 = r(3, 19); //1
|
||||
let r3 = r(4, 5); //2
|
||||
let r4 = r(7, 18); //2
|
||||
let r5 = r(9, 10); //3
|
||||
let r6 = r(12, 17); //4
|
||||
let r7 = r(13, 16); //5
|
||||
let r8 = r(14, 15); //6
|
||||
let r9 = r(22, 23); //0
|
||||
|
||||
let model = createTextModel(lines.join('\n'));
|
||||
let ranges = [r1, r2, r3, r4, r5, r6, r7, r8, r9];
|
||||
let providers = [new TestFoldingRangeProvider(model, ranges)];
|
||||
|
||||
async function assertLimit(maxEntries: number, expectedRanges: IndentRange[], message: string) {
|
||||
let indentRanges = await new SyntaxRangeProvider(model, providers, () => { }, maxEntries).compute(CancellationToken.None);
|
||||
let actual: IndentRange[] = [];
|
||||
if (indentRanges) {
|
||||
for (let i = 0; i < indentRanges.length; i++) {
|
||||
actual.push({ start: indentRanges.getStartLineNumber(i), end: indentRanges.getEndLineNumber(i) });
|
||||
}
|
||||
}
|
||||
assert.deepEqual(actual, expectedRanges, message);
|
||||
}
|
||||
|
||||
await assertLimit(1000, [r1, r2, r3, r4, r5, r6, r7, r8, r9], '1000');
|
||||
await assertLimit(9, [r1, r2, r3, r4, r5, r6, r7, r8, r9], '9');
|
||||
await assertLimit(8, [r1, r2, r3, r4, r5, r6, r7, r9], '8');
|
||||
await assertLimit(7, [r1, r2, r3, r4, r5, r6, r9], '7');
|
||||
await assertLimit(6, [r1, r2, r3, r4, r5, r9], '6');
|
||||
await assertLimit(5, [r1, r2, r3, r4, r9], '5');
|
||||
await assertLimit(4, [r1, r2, r3, r9], '4');
|
||||
await assertLimit(3, [r1, r2, r9], '3');
|
||||
await assertLimit(2, [r1, r9], '2');
|
||||
await assertLimit(1, [r1], '1');
|
||||
await assertLimit(0, [], '0');
|
||||
});
|
||||
|
||||
});
|
||||
61
lib/vscode/src/vs/editor/contrib/fontZoom/fontZoom.ts
Normal file
61
lib/vscode/src/vs/editor/contrib/fontZoom/fontZoom.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, ServicesAccessor, registerEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { EditorZoom } from 'vs/editor/common/config/editorZoom';
|
||||
|
||||
class EditorFontZoomIn extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.fontZoomIn',
|
||||
label: nls.localize('EditorFontZoomIn.label', "Editor Font Zoom In"),
|
||||
alias: 'Editor Font Zoom In',
|
||||
precondition: undefined
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
EditorZoom.setZoomLevel(EditorZoom.getZoomLevel() + 1);
|
||||
}
|
||||
}
|
||||
|
||||
class EditorFontZoomOut extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.fontZoomOut',
|
||||
label: nls.localize('EditorFontZoomOut.label', "Editor Font Zoom Out"),
|
||||
alias: 'Editor Font Zoom Out',
|
||||
precondition: undefined
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
EditorZoom.setZoomLevel(EditorZoom.getZoomLevel() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
class EditorFontZoomReset extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.fontZoomReset',
|
||||
label: nls.localize('EditorFontZoomReset.label', "Editor Font Zoom Reset"),
|
||||
alias: 'Editor Font Zoom Reset',
|
||||
precondition: undefined
|
||||
});
|
||||
}
|
||||
|
||||
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
|
||||
EditorZoom.setZoomLevel(0);
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(EditorFontZoomIn);
|
||||
registerEditorAction(EditorFontZoomOut);
|
||||
registerEditorAction(EditorFontZoomReset);
|
||||
411
lib/vscode/src/vs/editor/contrib/format/format.ts
Normal file
411
lib/vscode/src/vs/editor/contrib/format/format.ts
Normal file
@@ -0,0 +1,411 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { asArray, isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { illegalArgument, onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { CodeEditorStateFlag, EditorStateCancellationTokenSource, TextModelCancellationTokenSource } from 'vs/editor/browser/core/editorState';
|
||||
import { IActiveCodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
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 { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { ISingleEditOperation, ITextModel } from 'vs/editor/common/model';
|
||||
import { DocumentFormattingEditProvider, DocumentFormattingEditProviderRegistry, DocumentRangeFormattingEditProvider, DocumentRangeFormattingEditProviderRegistry, FormattingOptions, OnTypeFormattingEditProviderRegistry, TextEdit } from 'vs/editor/common/modes';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { LinkedList } from 'vs/base/common/linkedList';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { assertType } from 'vs/base/common/types';
|
||||
import { IProgress } from 'vs/platform/progress/common/progress';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
|
||||
export function alertFormattingEdits(edits: 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getRealAndSyntheticDocumentFormattersOrdered(model: ITextModel): DocumentFormattingEditProvider[] {
|
||||
const result: DocumentFormattingEditProvider[] = [];
|
||||
const seen = new Set<string>();
|
||||
|
||||
// (1) add all document formatter
|
||||
const docFormatter = DocumentFormattingEditProviderRegistry.ordered(model);
|
||||
for (const formatter of docFormatter) {
|
||||
result.push(formatter);
|
||||
if (formatter.extensionId) {
|
||||
seen.add(ExtensionIdentifier.toKey(formatter.extensionId));
|
||||
}
|
||||
}
|
||||
|
||||
// (2) add all range formatter as document formatter (unless the same extension already did that)
|
||||
const rangeFormatter = DocumentRangeFormattingEditProviderRegistry.ordered(model);
|
||||
for (const formatter of rangeFormatter) {
|
||||
if (formatter.extensionId) {
|
||||
if (seen.has(ExtensionIdentifier.toKey(formatter.extensionId))) {
|
||||
continue;
|
||||
}
|
||||
seen.add(ExtensionIdentifier.toKey(formatter.extensionId));
|
||||
}
|
||||
result.push({
|
||||
displayName: formatter.displayName,
|
||||
extensionId: formatter.extensionId,
|
||||
provideDocumentFormattingEdits(model, options, token) {
|
||||
return formatter.provideDocumentRangeFormattingEdits(model, model.getFullModelRange(), options, token);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export const enum FormattingMode {
|
||||
Explicit = 1,
|
||||
Silent = 2
|
||||
}
|
||||
|
||||
export interface IFormattingEditProviderSelector {
|
||||
<T extends (DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider)>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined>;
|
||||
}
|
||||
|
||||
export abstract class FormattingConflicts {
|
||||
|
||||
private static readonly _selectors = new LinkedList<IFormattingEditProviderSelector>();
|
||||
|
||||
static setFormatterSelector(selector: IFormattingEditProviderSelector): IDisposable {
|
||||
const remove = FormattingConflicts._selectors.unshift(selector);
|
||||
return { dispose: remove };
|
||||
}
|
||||
|
||||
static async select<T extends (DocumentFormattingEditProvider | DocumentRangeFormattingEditProvider)>(formatter: T[], document: ITextModel, mode: FormattingMode): Promise<T | undefined> {
|
||||
if (formatter.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const selector = Iterable.first(FormattingConflicts._selectors);
|
||||
if (selector) {
|
||||
return await selector(formatter, document, mode);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export async function formatDocumentRangesWithSelectedProvider(
|
||||
accessor: ServicesAccessor,
|
||||
editorOrModel: ITextModel | IActiveCodeEditor,
|
||||
rangeOrRanges: Range | Range[],
|
||||
mode: FormattingMode,
|
||||
progress: IProgress<DocumentRangeFormattingEditProvider>,
|
||||
token: CancellationToken
|
||||
): Promise<void> {
|
||||
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
|
||||
const provider = DocumentRangeFormattingEditProviderRegistry.ordered(model);
|
||||
const selected = await FormattingConflicts.select(provider, model, mode);
|
||||
if (selected) {
|
||||
progress.report(selected);
|
||||
await instaService.invokeFunction(formatDocumentRangesWithProvider, selected, editorOrModel, rangeOrRanges, token);
|
||||
}
|
||||
}
|
||||
|
||||
export async function formatDocumentRangesWithProvider(
|
||||
accessor: ServicesAccessor,
|
||||
provider: DocumentRangeFormattingEditProvider,
|
||||
editorOrModel: ITextModel | IActiveCodeEditor,
|
||||
rangeOrRanges: Range | Range[],
|
||||
token: CancellationToken
|
||||
): Promise<boolean> {
|
||||
const workerService = accessor.get(IEditorWorkerService);
|
||||
|
||||
let model: ITextModel;
|
||||
let cts: CancellationTokenSource;
|
||||
if (isCodeEditor(editorOrModel)) {
|
||||
model = editorOrModel.getModel();
|
||||
cts = new EditorStateCancellationTokenSource(editorOrModel, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position, undefined, token);
|
||||
} else {
|
||||
model = editorOrModel;
|
||||
cts = new TextModelCancellationTokenSource(editorOrModel, token);
|
||||
}
|
||||
|
||||
// make sure that ranges don't overlap nor touch each other
|
||||
let ranges: Range[] = [];
|
||||
let len = 0;
|
||||
for (let range of asArray(rangeOrRanges).sort(Range.compareRangesUsingStarts)) {
|
||||
if (len > 0 && Range.areIntersectingOrTouching(ranges[len - 1], range)) {
|
||||
ranges[len - 1] = Range.fromPositions(ranges[len - 1].getStartPosition(), range.getEndPosition());
|
||||
} else {
|
||||
len = ranges.push(range);
|
||||
}
|
||||
}
|
||||
|
||||
const allEdits: TextEdit[] = [];
|
||||
for (let range of ranges) {
|
||||
try {
|
||||
const rawEdits = await provider.provideDocumentRangeFormattingEdits(
|
||||
model,
|
||||
range,
|
||||
model.getFormattingOptions(),
|
||||
cts.token
|
||||
);
|
||||
const minEdits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
|
||||
if (minEdits) {
|
||||
allEdits.push(...minEdits);
|
||||
}
|
||||
if (cts.token.isCancellationRequested) {
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
cts.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
if (allEdits.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isCodeEditor(editorOrModel)) {
|
||||
// use editor to apply edits
|
||||
FormattingEdit.execute(editorOrModel, allEdits, true);
|
||||
alertFormattingEdits(allEdits);
|
||||
editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate);
|
||||
|
||||
} else {
|
||||
// use model to apply edits
|
||||
const [{ range }] = allEdits;
|
||||
const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
|
||||
model.pushEditOperations([initialSelection], allEdits.map(edit => {
|
||||
return {
|
||||
text: edit.text,
|
||||
range: Range.lift(edit.range),
|
||||
forceMoveMarkers: true
|
||||
};
|
||||
}), undoEdits => {
|
||||
for (const { range } of undoEdits) {
|
||||
if (Range.areIntersectingOrTouching(range, initialSelection)) {
|
||||
return [new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function formatDocumentWithSelectedProvider(
|
||||
accessor: ServicesAccessor,
|
||||
editorOrModel: ITextModel | IActiveCodeEditor,
|
||||
mode: FormattingMode,
|
||||
progress: IProgress<DocumentFormattingEditProvider>,
|
||||
token: CancellationToken
|
||||
): Promise<void> {
|
||||
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
|
||||
const provider = getRealAndSyntheticDocumentFormattersOrdered(model);
|
||||
const selected = await FormattingConflicts.select(provider, model, mode);
|
||||
if (selected) {
|
||||
progress.report(selected);
|
||||
await instaService.invokeFunction(formatDocumentWithProvider, selected, editorOrModel, mode, token);
|
||||
}
|
||||
}
|
||||
|
||||
export async function formatDocumentWithProvider(
|
||||
accessor: ServicesAccessor,
|
||||
provider: DocumentFormattingEditProvider,
|
||||
editorOrModel: ITextModel | IActiveCodeEditor,
|
||||
mode: FormattingMode,
|
||||
token: CancellationToken
|
||||
): Promise<boolean> {
|
||||
const workerService = accessor.get(IEditorWorkerService);
|
||||
|
||||
let model: ITextModel;
|
||||
let cts: CancellationTokenSource;
|
||||
if (isCodeEditor(editorOrModel)) {
|
||||
model = editorOrModel.getModel();
|
||||
cts = new EditorStateCancellationTokenSource(editorOrModel, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position, undefined, token);
|
||||
} else {
|
||||
model = editorOrModel;
|
||||
cts = new TextModelCancellationTokenSource(editorOrModel, token);
|
||||
}
|
||||
|
||||
let edits: TextEdit[] | undefined;
|
||||
try {
|
||||
const rawEdits = await provider.provideDocumentFormattingEdits(
|
||||
model,
|
||||
model.getFormattingOptions(),
|
||||
cts.token
|
||||
);
|
||||
|
||||
edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
|
||||
|
||||
if (cts.token.isCancellationRequested) {
|
||||
return true;
|
||||
}
|
||||
|
||||
} finally {
|
||||
cts.dispose();
|
||||
}
|
||||
|
||||
if (!edits || edits.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isCodeEditor(editorOrModel)) {
|
||||
// use editor to apply edits
|
||||
FormattingEdit.execute(editorOrModel, edits, mode !== FormattingMode.Silent);
|
||||
|
||||
if (mode !== FormattingMode.Silent) {
|
||||
alertFormattingEdits(edits);
|
||||
editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), ScrollType.Immediate);
|
||||
}
|
||||
|
||||
} else {
|
||||
// use model to apply edits
|
||||
const [{ range }] = edits;
|
||||
const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
|
||||
model.pushEditOperations([initialSelection], edits.map(edit => {
|
||||
return {
|
||||
text: edit.text,
|
||||
range: Range.lift(edit.range),
|
||||
forceMoveMarkers: true
|
||||
};
|
||||
}), undoEdits => {
|
||||
for (const { range } of undoEdits) {
|
||||
if (Range.areIntersectingOrTouching(range, initialSelection)) {
|
||||
return [new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function getDocumentRangeFormattingEditsUntilResult(
|
||||
workerService: IEditorWorkerService,
|
||||
model: ITextModel,
|
||||
range: Range,
|
||||
options: FormattingOptions,
|
||||
token: CancellationToken
|
||||
): Promise<TextEdit[] | undefined> {
|
||||
|
||||
const providers = DocumentRangeFormattingEditProviderRegistry.ordered(model);
|
||||
for (const provider of providers) {
|
||||
let rawEdits = await Promise.resolve(provider.provideDocumentRangeFormattingEdits(model, range, options, token)).catch(onUnexpectedExternalError);
|
||||
if (isNonEmptyArray(rawEdits)) {
|
||||
return await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function getDocumentFormattingEditsUntilResult(
|
||||
workerService: IEditorWorkerService,
|
||||
model: ITextModel,
|
||||
options: FormattingOptions,
|
||||
token: CancellationToken
|
||||
): Promise<TextEdit[] | undefined> {
|
||||
|
||||
const providers = getRealAndSyntheticDocumentFormattersOrdered(model);
|
||||
for (const provider of providers) {
|
||||
let rawEdits = await Promise.resolve(provider.provideDocumentFormattingEdits(model, options, token)).catch(onUnexpectedExternalError);
|
||||
if (isNonEmptyArray(rawEdits)) {
|
||||
return await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getOnTypeFormattingEdits(
|
||||
workerService: IEditorWorkerService,
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
ch: string,
|
||||
options: FormattingOptions
|
||||
): Promise<TextEdit[] | null | undefined> {
|
||||
|
||||
const providers = OnTypeFormattingEditProviderRegistry.ordered(model);
|
||||
|
||||
if (providers.length === 0) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
if (providers[0].autoFormatTriggerCharacters.indexOf(ch) < 0) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return Promise.resolve(providers[0].provideOnTypeFormattingEdits(model, position, ch, options, CancellationToken.None)).catch(onUnexpectedExternalError).then(edits => {
|
||||
return workerService.computeMoreMinimalEdits(model.uri, edits);
|
||||
});
|
||||
}
|
||||
|
||||
CommandsRegistry.registerCommand('_executeFormatRangeProvider', function (accessor, ...args) {
|
||||
const [resource, range, options] = args;
|
||||
assertType(URI.isUri(resource));
|
||||
assertType(Range.isIRange(range));
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument('resource');
|
||||
}
|
||||
return getDocumentRangeFormattingEditsUntilResult(accessor.get(IEditorWorkerService), model, Range.lift(range), options, CancellationToken.None);
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand('_executeFormatDocumentProvider', function (accessor, ...args) {
|
||||
const [resource, options] = args;
|
||||
assertType(URI.isUri(resource));
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument('resource');
|
||||
}
|
||||
|
||||
return getDocumentFormattingEditsUntilResult(accessor.get(IEditorWorkerService), model, options, CancellationToken.None);
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand('_executeFormatOnTypeProvider', function (accessor, ...args) {
|
||||
const [resource, position, ch, options] = args;
|
||||
assertType(URI.isUri(resource));
|
||||
assertType(Position.isIPosition(position));
|
||||
assertType(typeof ch === 'string');
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (!model) {
|
||||
throw illegalArgument('resource');
|
||||
}
|
||||
|
||||
return getOnTypeFormattingEdits(accessor.get(IEditorWorkerService), model, Position.lift(position), ch, options);
|
||||
});
|
||||
303
lib/vscode/src/vs/editor/contrib/format/formatActions.ts
Normal file
303
lib/vscode/src/vs/editor/contrib/format/formatActions.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { CharacterSet } from 'vs/editor/common/core/characterClassifier';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { DocumentRangeFormattingEditProviderRegistry, OnTypeFormattingEditProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
|
||||
import { getOnTypeFormattingEdits, alertFormattingEdits, formatDocumentRangesWithSelectedProvider, formatDocumentWithSelectedProvider, FormattingMode } from 'vs/editor/contrib/format/format';
|
||||
import { FormattingEdit } from 'vs/editor/contrib/format/formattingEdit';
|
||||
import * as nls from 'vs/nls';
|
||||
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { Progress, IEditorProgressService } from 'vs/platform/progress/common/progress';
|
||||
|
||||
class FormatOnType implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.autoFormat';
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
private readonly _callOnDispose = new DisposableStore();
|
||||
private readonly _callOnModel = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IEditorWorkerService private readonly _workerService: IEditorWorkerService
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._callOnDispose.add(editor.onDidChangeConfiguration(() => this._update()));
|
||||
this._callOnDispose.add(editor.onDidChangeModel(() => this._update()));
|
||||
this._callOnDispose.add(editor.onDidChangeModelLanguage(() => this._update()));
|
||||
this._callOnDispose.add(OnTypeFormattingEditProviderRegistry.onDidChange(this._update, this));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._callOnDispose.dispose();
|
||||
this._callOnModel.dispose();
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
|
||||
// clean up
|
||||
this._callOnModel.clear();
|
||||
|
||||
// we are disabled
|
||||
if (!this._editor.getOption(EditorOption.formatOnType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no model
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
|
||||
// no support
|
||||
const [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.add(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.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._editor.getSelections().length > 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const model = this._editor.getModel();
|
||||
const position = this._editor.getPosition();
|
||||
let 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
|
||||
const 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;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
getOnTypeFormattingEdits(
|
||||
this._workerService,
|
||||
model,
|
||||
position,
|
||||
ch,
|
||||
model.getFormattingOptions()
|
||||
).then(edits => {
|
||||
|
||||
unbind.dispose();
|
||||
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNonEmptyArray(edits)) {
|
||||
FormattingEdit.execute(this._editor, edits, true);
|
||||
alertFormattingEdits(edits);
|
||||
}
|
||||
|
||||
}, (err) => {
|
||||
unbind.dispose();
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class FormatOnPaste implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.formatOnPaste';
|
||||
|
||||
private readonly _callOnDispose = new DisposableStore();
|
||||
private readonly _callOnModel = new DisposableStore();
|
||||
|
||||
constructor(
|
||||
private readonly editor: ICodeEditor,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
this._callOnDispose.add(editor.onDidChangeConfiguration(() => this._update()));
|
||||
this._callOnDispose.add(editor.onDidChangeModel(() => this._update()));
|
||||
this._callOnDispose.add(editor.onDidChangeModelLanguage(() => this._update()));
|
||||
this._callOnDispose.add(DocumentRangeFormattingEditProviderRegistry.onDidChange(this._update, this));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._callOnDispose.dispose();
|
||||
this._callOnModel.dispose();
|
||||
}
|
||||
|
||||
private _update(): void {
|
||||
|
||||
// clean up
|
||||
this._callOnModel.clear();
|
||||
|
||||
// we are disabled
|
||||
if (!this.editor.getOption(EditorOption.formatOnPaste)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no model
|
||||
if (!this.editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no formatter
|
||||
if (!DocumentRangeFormattingEditProviderRegistry.has(this.editor.getModel())) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._callOnModel.add(this.editor.onDidPaste(({ range }) => this._trigger(range)));
|
||||
}
|
||||
|
||||
private _trigger(range: Range): void {
|
||||
if (!this.editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
if (this.editor.getSelections().length > 1) {
|
||||
return;
|
||||
}
|
||||
this._instantiationService.invokeFunction(formatDocumentRangesWithSelectedProvider, this.editor, range, FormattingMode.Silent, Progress.None, CancellationToken.None).catch(onUnexpectedError);
|
||||
}
|
||||
}
|
||||
|
||||
class FormatDocumentAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.formatDocument',
|
||||
label: nls.localize('formatDocument.label', "Format Document"),
|
||||
alias: 'Format Document',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.notInCompositeEditor, EditorContextKeys.writable, EditorContextKeys.hasDocumentFormattingProvider),
|
||||
kbOpts: {
|
||||
kbExpr: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, EditorContextKeys.hasDocumentFormattingProvider),
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.KEY_F,
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_I },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
contextMenuOpts: {
|
||||
when: EditorContextKeys.hasDocumentFormattingProvider,
|
||||
group: '1_modification',
|
||||
order: 1.3
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
if (editor.hasModel()) {
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const progressService = accessor.get(IEditorProgressService);
|
||||
await progressService.showWhile(
|
||||
instaService.invokeFunction(formatDocumentWithSelectedProvider, editor, FormattingMode.Explicit, Progress.None, CancellationToken.None),
|
||||
250
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FormatSelectionAction extends EditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.action.formatSelection',
|
||||
label: nls.localize('formatSelection.label', "Format Selection"),
|
||||
alias: 'Format Selection',
|
||||
precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasDocumentSelectionFormattingProvider),
|
||||
kbOpts: {
|
||||
kbExpr: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, EditorContextKeys.hasDocumentSelectionFormattingProvider),
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_F),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
contextMenuOpts: {
|
||||
when: ContextKeyExpr.and(EditorContextKeys.hasDocumentSelectionFormattingProvider, EditorContextKeys.hasNonEmptySelection),
|
||||
group: '1_modification',
|
||||
order: 1.31
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
if (!editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
const instaService = accessor.get(IInstantiationService);
|
||||
const model = editor.getModel();
|
||||
|
||||
const ranges = editor.getSelections().map(range => {
|
||||
return range.isEmpty()
|
||||
? new Range(range.startLineNumber, 1, range.startLineNumber, model.getLineMaxColumn(range.startLineNumber))
|
||||
: range;
|
||||
});
|
||||
|
||||
const progressService = accessor.get(IEditorProgressService);
|
||||
await progressService.showWhile(
|
||||
instaService.invokeFunction(formatDocumentRangesWithSelectedProvider, editor, ranges, FormattingMode.Explicit, Progress.None, CancellationToken.None),
|
||||
250
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(FormatOnType.ID, FormatOnType);
|
||||
registerEditorContribution(FormatOnPaste.ID, FormatOnPaste);
|
||||
registerEditorAction(FormatDocumentAction);
|
||||
registerEditorAction(FormatSelectionAction);
|
||||
|
||||
// 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', async accessor => {
|
||||
const editor = accessor.get(ICodeEditorService).getFocusedCodeEditor();
|
||||
if (!editor || !editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
const commandService = accessor.get(ICommandService);
|
||||
if (editor.getSelection().isEmpty()) {
|
||||
await commandService.executeCommand('editor.action.formatDocument');
|
||||
} else {
|
||||
await commandService.executeCommand('editor.action.formatSelection');
|
||||
}
|
||||
});
|
||||
61
lib/vscode/src/vs/editor/contrib/format/formattingEdit.ts
Normal file
61
lib/vscode/src/vs/editor/contrib/format/formattingEdit.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditOperation } from 'vs/editor/common/core/editOperation';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { EndOfLineSequence, ISingleEditOperation } from 'vs/editor/common/model';
|
||||
import { TextEdit } from 'vs/editor/common/modes';
|
||||
|
||||
export class FormattingEdit {
|
||||
|
||||
private static _handleEolEdits(editor: ICodeEditor, edits: TextEdit[]): ISingleEditOperation[] {
|
||||
let newEol: EndOfLineSequence | undefined = undefined;
|
||||
let singleEdits: ISingleEditOperation[] = [];
|
||||
|
||||
for (let edit of edits) {
|
||||
if (typeof edit.eol === 'number') {
|
||||
newEol = edit.eol;
|
||||
}
|
||||
if (edit.range && typeof edit.text === 'string') {
|
||||
singleEdits.push(edit);
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof newEol === 'number') {
|
||||
if (editor.hasModel()) {
|
||||
editor.getModel().pushEOL(newEol);
|
||||
}
|
||||
}
|
||||
|
||||
return singleEdits;
|
||||
}
|
||||
|
||||
private static _isFullModelReplaceEdit(editor: ICodeEditor, edit: ISingleEditOperation): boolean {
|
||||
if (!editor.hasModel()) {
|
||||
return false;
|
||||
}
|
||||
const model = editor.getModel();
|
||||
const editRange = model.validateRange(edit.range);
|
||||
const fullModelRange = model.getFullModelRange();
|
||||
return fullModelRange.equalsRange(editRange);
|
||||
}
|
||||
|
||||
static execute(editor: ICodeEditor, _edits: TextEdit[], addUndoStops: boolean) {
|
||||
if (addUndoStops) {
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
const edits = FormattingEdit._handleEolEdits(editor, _edits);
|
||||
if (edits.length === 1 && FormattingEdit._isFullModelReplaceEdit(editor, edits[0])) {
|
||||
// We use replace semantics and hope that markers stay put...
|
||||
editor.executeEdits('formatEditsCommand', edits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
|
||||
} else {
|
||||
editor.executeEdits('formatEditsCommand', edits.map(edit => EditOperation.replaceMove(Range.lift(edit.range), edit.text)));
|
||||
}
|
||||
if (addUndoStops) {
|
||||
editor.pushUndoStop();
|
||||
}
|
||||
}
|
||||
}
|
||||
299
lib/vscode/src/vs/editor/contrib/gotoError/gotoError.ts
Normal file
299
lib/vscode/src/vs/editor/contrib/gotoError/gotoError.ts
Normal file
@@ -0,0 +1,299 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IMarker } from 'vs/platform/markers/common/markers';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { registerEditorAction, registerEditorContribution, ServicesAccessor, IActionOptions, EditorAction, EditorCommand, registerEditorCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { MarkerNavigationWidget } from './gotoErrorWidget';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { MenuId } from 'vs/platform/actions/common/actions';
|
||||
import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IMarkerNavigationService, MarkerList } from 'vs/editor/contrib/gotoError/markerNavigationService';
|
||||
|
||||
export class MarkerController implements IEditorContribution {
|
||||
|
||||
static readonly ID = 'editor.contrib.markerController';
|
||||
|
||||
static get(editor: ICodeEditor): MarkerController {
|
||||
return editor.getContribution<MarkerController>(MarkerController.ID);
|
||||
}
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
|
||||
private readonly _widgetVisible: IContextKey<boolean>;
|
||||
private readonly _sessionDispoables = new DisposableStore();
|
||||
|
||||
private _model?: MarkerList;
|
||||
private _widget?: MarkerNavigationWidget;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IMarkerNavigationService private readonly _markerNavigationService: IMarkerNavigationService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
) {
|
||||
this._editor = editor;
|
||||
this._widgetVisible = CONTEXT_MARKERS_NAVIGATION_VISIBLE.bindTo(this._contextKeyService);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._cleanUp();
|
||||
this._sessionDispoables.dispose();
|
||||
}
|
||||
|
||||
private _cleanUp(): void {
|
||||
this._widgetVisible.reset();
|
||||
this._sessionDispoables.clear();
|
||||
this._widget = undefined;
|
||||
this._model = undefined;
|
||||
}
|
||||
|
||||
private _getOrCreateModel(uri: URI | undefined): MarkerList {
|
||||
|
||||
if (this._model && this._model.matches(uri)) {
|
||||
return this._model;
|
||||
}
|
||||
let reusePosition = false;
|
||||
if (this._model) {
|
||||
reusePosition = true;
|
||||
this._cleanUp();
|
||||
}
|
||||
|
||||
this._model = this._markerNavigationService.getMarkerList(uri);
|
||||
if (reusePosition) {
|
||||
this._model.move(true, this._editor.getModel()!, this._editor.getPosition()!);
|
||||
}
|
||||
|
||||
this._widget = this._instantiationService.createInstance(MarkerNavigationWidget, this._editor);
|
||||
this._widget.onDidClose(() => this.close(), this, this._sessionDispoables);
|
||||
this._widgetVisible.set(true);
|
||||
|
||||
this._sessionDispoables.add(this._model);
|
||||
this._sessionDispoables.add(this._widget);
|
||||
|
||||
// follow cursor
|
||||
this._sessionDispoables.add(this._editor.onDidChangeCursorPosition(e => {
|
||||
if (!this._model?.selected || !Range.containsPosition(this._model?.selected.marker, e.position)) {
|
||||
this._model?.resetIndex();
|
||||
}
|
||||
}));
|
||||
|
||||
// update markers
|
||||
this._sessionDispoables.add(this._model.onDidChange(() => {
|
||||
if (!this._widget || !this._widget.position || !this._model) {
|
||||
return;
|
||||
}
|
||||
const info = this._model.find(this._editor.getModel()!.uri, this._widget!.position!);
|
||||
if (info) {
|
||||
this._widget.updateMarker(info.marker);
|
||||
} else {
|
||||
this._widget.showStale();
|
||||
}
|
||||
}));
|
||||
|
||||
// open related
|
||||
this._sessionDispoables.add(this._widget.onDidSelectRelatedInformation(related => {
|
||||
this._editorService.openCodeEditor({
|
||||
resource: related.resource,
|
||||
options: { pinned: true, revealIfOpened: true, selection: Range.lift(related).collapseToStart() }
|
||||
}, this._editor);
|
||||
this.close(false);
|
||||
}));
|
||||
this._sessionDispoables.add(this._editor.onDidChangeModel(() => this._cleanUp()));
|
||||
|
||||
return this._model;
|
||||
}
|
||||
|
||||
close(focusEditor: boolean = true): void {
|
||||
this._cleanUp();
|
||||
if (focusEditor) {
|
||||
this._editor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
showAtMarker(marker: IMarker): void {
|
||||
if (this._editor.hasModel()) {
|
||||
const model = this._getOrCreateModel(this._editor.getModel().uri);
|
||||
model.resetIndex();
|
||||
model.move(true, this._editor.getModel(), new Position(marker.startLineNumber, marker.startColumn));
|
||||
if (model.selected) {
|
||||
this._widget!.showAtMarker(model.selected.marker, model.selected.index, model.selected.total);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async nagivate(next: boolean, multiFile: boolean) {
|
||||
if (this._editor.hasModel()) {
|
||||
const model = this._getOrCreateModel(multiFile ? undefined : this._editor.getModel().uri);
|
||||
model.move(next, this._editor.getModel(), this._editor.getPosition());
|
||||
if (!model.selected) {
|
||||
return;
|
||||
}
|
||||
if (model.selected.marker.resource.toString() !== this._editor.getModel().uri.toString()) {
|
||||
// show in different editor
|
||||
this._cleanUp();
|
||||
const otherEditor = await this._editorService.openCodeEditor({
|
||||
resource: model.selected.marker.resource,
|
||||
options: { pinned: false, revealIfOpened: true, selectionRevealType: TextEditorSelectionRevealType.NearTop, selection: model.selected.marker }
|
||||
}, this._editor);
|
||||
|
||||
if (otherEditor) {
|
||||
MarkerController.get(otherEditor).close();
|
||||
MarkerController.get(otherEditor).nagivate(next, multiFile);
|
||||
}
|
||||
|
||||
} else {
|
||||
// show in this editor
|
||||
this._widget!.showAtMarker(model.selected.marker, model.selected.index, model.selected.total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MarkerNavigationAction extends EditorAction {
|
||||
|
||||
constructor(
|
||||
private readonly _next: boolean,
|
||||
private readonly _multiFile: boolean,
|
||||
opts: IActionOptions
|
||||
) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
async run(_accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
if (editor.hasModel()) {
|
||||
MarkerController.get(editor).nagivate(this._next, this._multiFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NextMarkerAction extends MarkerNavigationAction {
|
||||
static ID: string = 'editor.action.marker.next';
|
||||
static LABEL: string = nls.localize('markerAction.next.label', "Go to Next Problem (Error, Warning, Info)");
|
||||
constructor() {
|
||||
super(true, false, {
|
||||
id: NextMarkerAction.ID,
|
||||
label: NextMarkerAction.LABEL,
|
||||
alias: 'Go to Next Problem (Error, Warning, Info)',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.Alt | KeyCode.F8,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MarkerNavigationWidget.TitleMenu,
|
||||
title: NextMarkerAction.LABEL,
|
||||
icon: registerIcon('marker-navigation-next', Codicon.chevronDown),
|
||||
group: 'navigation',
|
||||
order: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PrevMarkerAction extends MarkerNavigationAction {
|
||||
static ID: string = 'editor.action.marker.prev';
|
||||
static LABEL: string = nls.localize('markerAction.previous.label', "Go to Previous Problem (Error, Warning, Info)");
|
||||
constructor() {
|
||||
super(false, false, {
|
||||
id: PrevMarkerAction.ID,
|
||||
label: PrevMarkerAction.LABEL,
|
||||
alias: 'Go to Previous Problem (Error, Warning, Info)',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.Shift | KeyMod.Alt | KeyCode.F8,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MarkerNavigationWidget.TitleMenu,
|
||||
title: NextMarkerAction.LABEL,
|
||||
icon: registerIcon('marker-navigation-previous', Codicon.chevronUp),
|
||||
group: 'navigation',
|
||||
order: 2
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class NextMarkerInFilesAction extends MarkerNavigationAction {
|
||||
constructor() {
|
||||
super(true, true, {
|
||||
id: 'editor.action.marker.nextInFiles',
|
||||
label: nls.localize('markerAction.nextInFiles.label', "Go to Next Problem in Files (Error, Warning, Info)"),
|
||||
alias: 'Go to Next Problem in Files (Error, Warning, Info)',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyCode.F8,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarGoMenu,
|
||||
title: nls.localize({ key: 'miGotoNextProblem', comment: ['&& denotes a mnemonic'] }, "Next &&Problem"),
|
||||
group: '6_problem_nav',
|
||||
order: 1
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class PrevMarkerInFilesAction extends MarkerNavigationAction {
|
||||
constructor() {
|
||||
super(false, true, {
|
||||
id: 'editor.action.marker.prevInFiles',
|
||||
label: nls.localize('markerAction.previousInFiles.label', "Go to Previous Problem in Files (Error, Warning, Info)"),
|
||||
alias: 'Go to Previous Problem in Files (Error, Warning, Info)',
|
||||
precondition: undefined,
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyMod.Shift | KeyCode.F8,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarGoMenu,
|
||||
title: nls.localize({ key: 'miGotoPreviousProblem', comment: ['&& denotes a mnemonic'] }, "Previous &&Problem"),
|
||||
group: '6_problem_nav',
|
||||
order: 2
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(MarkerController.ID, MarkerController);
|
||||
registerEditorAction(NextMarkerAction);
|
||||
registerEditorAction(PrevMarkerAction);
|
||||
registerEditorAction(NextMarkerInFilesAction);
|
||||
registerEditorAction(PrevMarkerInFilesAction);
|
||||
|
||||
const CONTEXT_MARKERS_NAVIGATION_VISIBLE = new RawContextKey<boolean>('markersNavigationVisible', false);
|
||||
|
||||
const MarkerCommand = EditorCommand.bindToContribution<MarkerController>(MarkerController.get);
|
||||
|
||||
registerEditorCommand(new MarkerCommand({
|
||||
id: 'closeMarkersNavigation',
|
||||
precondition: CONTEXT_MARKERS_NAVIGATION_VISIBLE,
|
||||
handler: x => x.close(),
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib + 50,
|
||||
kbExpr: EditorContextKeys.focus,
|
||||
primary: KeyCode.Escape,
|
||||
secondary: [KeyMod.Shift | KeyCode.Escape]
|
||||
}
|
||||
}));
|
||||
413
lib/vscode/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts
Normal file
413
lib/vscode/src/vs/editor/contrib/gotoError/gotoErrorWidget.ts
Normal file
@@ -0,0 +1,413 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./media/gotoErrorWidget';
|
||||
import * as nls from 'vs/nls';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { dispose, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IMarker, MarkerSeverity, IRelatedInformation } from 'vs/platform/markers/common/markers';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerColor, oneOf, textLinkForeground, editorErrorForeground, editorErrorBorder, editorWarningForeground, editorWarningBorder, editorInfoForeground, editorInfoBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IThemeService, IColorTheme, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { getBaseLabel, getPathLabel } from 'vs/base/common/labels';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { PeekViewWidget, peekViewTitleForeground, peekViewTitleInfoForeground } from 'vs/editor/contrib/peekView/peekView';
|
||||
import { basename } from 'vs/base/common/resources';
|
||||
import { IAction } from 'vs/base/common/actions';
|
||||
import { IActionBarOptions, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { SeverityIcon } from 'vs/platform/severityIcon/common/severityIcon';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { IOpenerService } from 'vs/platform/opener/common/opener';
|
||||
import { MenuId, IMenuService } from 'vs/platform/actions/common/actions';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { createAndFillInActionBarActions } from 'vs/platform/actions/browser/menuEntryActionViewItem';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
class MessageWidget {
|
||||
|
||||
private _lines: number = 0;
|
||||
private _longestLineLength: number = 0;
|
||||
|
||||
private readonly _editor: ICodeEditor;
|
||||
private readonly _messageBlock: HTMLDivElement;
|
||||
private readonly _relatedBlock: HTMLDivElement;
|
||||
private readonly _scrollable: ScrollableElement;
|
||||
private readonly _relatedDiagnostics = new WeakMap<HTMLElement, IRelatedInformation>();
|
||||
private readonly _disposables: DisposableStore = new DisposableStore();
|
||||
|
||||
private _codeLink?: HTMLElement;
|
||||
|
||||
constructor(
|
||||
parent: HTMLElement,
|
||||
editor: ICodeEditor,
|
||||
onRelatedInformation: (related: IRelatedInformation) => void,
|
||||
private readonly _openerService: IOpenerService,
|
||||
) {
|
||||
this._editor = editor;
|
||||
|
||||
const domNode = document.createElement('div');
|
||||
domNode.className = 'descriptioncontainer';
|
||||
|
||||
this._messageBlock = document.createElement('div');
|
||||
this._messageBlock.classList.add('message');
|
||||
this._messageBlock.setAttribute('aria-live', 'assertive');
|
||||
this._messageBlock.setAttribute('role', 'alert');
|
||||
domNode.appendChild(this._messageBlock);
|
||||
|
||||
this._relatedBlock = document.createElement('div');
|
||||
domNode.appendChild(this._relatedBlock);
|
||||
this._disposables.add(dom.addStandardDisposableListener(this._relatedBlock, 'click', event => {
|
||||
event.preventDefault();
|
||||
const related = this._relatedDiagnostics.get(event.target);
|
||||
if (related) {
|
||||
onRelatedInformation(related);
|
||||
}
|
||||
}));
|
||||
|
||||
this._scrollable = new ScrollableElement(domNode, {
|
||||
horizontal: ScrollbarVisibility.Auto,
|
||||
vertical: ScrollbarVisibility.Auto,
|
||||
useShadows: false,
|
||||
horizontalScrollbarSize: 3,
|
||||
verticalScrollbarSize: 3
|
||||
});
|
||||
parent.appendChild(this._scrollable.getDomNode());
|
||||
this._disposables.add(this._scrollable.onScroll(e => {
|
||||
domNode.style.left = `-${e.scrollLeft}px`;
|
||||
domNode.style.top = `-${e.scrollTop}px`;
|
||||
}));
|
||||
this._disposables.add(this._scrollable);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._disposables);
|
||||
}
|
||||
|
||||
update(marker: IMarker): void {
|
||||
const { source, message, relatedInformation, code } = marker;
|
||||
let sourceAndCodeLength = (source?.length || 0) + '()'.length;
|
||||
if (code) {
|
||||
if (typeof code === 'string') {
|
||||
sourceAndCodeLength += code.length;
|
||||
} else {
|
||||
sourceAndCodeLength += code.value.length;
|
||||
}
|
||||
}
|
||||
|
||||
const lines = message.split(/\r\n|\r|\n/g);
|
||||
this._lines = lines.length;
|
||||
this._longestLineLength = 0;
|
||||
for (const line of lines) {
|
||||
this._longestLineLength = Math.max(line.length + sourceAndCodeLength, this._longestLineLength);
|
||||
}
|
||||
|
||||
dom.clearNode(this._messageBlock);
|
||||
this._messageBlock.setAttribute('aria-label', this.getAriaLabel(marker));
|
||||
this._editor.applyFontInfo(this._messageBlock);
|
||||
let lastLineElement = this._messageBlock;
|
||||
for (const line of lines) {
|
||||
lastLineElement = document.createElement('div');
|
||||
lastLineElement.innerText = line;
|
||||
if (line === '') {
|
||||
lastLineElement.style.height = this._messageBlock.style.lineHeight;
|
||||
}
|
||||
this._messageBlock.appendChild(lastLineElement);
|
||||
}
|
||||
if (source || code) {
|
||||
const detailsElement = document.createElement('span');
|
||||
detailsElement.classList.add('details');
|
||||
lastLineElement.appendChild(detailsElement);
|
||||
if (source) {
|
||||
const sourceElement = document.createElement('span');
|
||||
sourceElement.innerText = source;
|
||||
sourceElement.classList.add('source');
|
||||
detailsElement.appendChild(sourceElement);
|
||||
}
|
||||
if (code) {
|
||||
if (typeof code === 'string') {
|
||||
const codeElement = document.createElement('span');
|
||||
codeElement.innerText = `(${code})`;
|
||||
codeElement.classList.add('code');
|
||||
detailsElement.appendChild(codeElement);
|
||||
} else {
|
||||
this._codeLink = dom.$('a.code-link');
|
||||
this._codeLink.setAttribute('href', `${code.target.toString()}`);
|
||||
|
||||
this._codeLink.onclick = (e) => {
|
||||
this._openerService.open(code.target);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const codeElement = dom.append(this._codeLink, dom.$('span'));
|
||||
codeElement.innerText = code.value;
|
||||
detailsElement.appendChild(this._codeLink);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dom.clearNode(this._relatedBlock);
|
||||
this._editor.applyFontInfo(this._relatedBlock);
|
||||
if (isNonEmptyArray(relatedInformation)) {
|
||||
const relatedInformationNode = this._relatedBlock.appendChild(document.createElement('div'));
|
||||
relatedInformationNode.style.paddingTop = `${Math.floor(this._editor.getOption(EditorOption.lineHeight) * 0.66)}px`;
|
||||
this._lines += 1;
|
||||
|
||||
for (const related of relatedInformation) {
|
||||
|
||||
let container = document.createElement('div');
|
||||
|
||||
let relatedResource = document.createElement('a');
|
||||
relatedResource.classList.add('filename');
|
||||
relatedResource.innerText = `${getBaseLabel(related.resource)}(${related.startLineNumber}, ${related.startColumn}): `;
|
||||
relatedResource.title = getPathLabel(related.resource, undefined);
|
||||
this._relatedDiagnostics.set(relatedResource, related);
|
||||
|
||||
let relatedMessage = document.createElement('span');
|
||||
relatedMessage.innerText = related.message;
|
||||
|
||||
container.appendChild(relatedResource);
|
||||
container.appendChild(relatedMessage);
|
||||
|
||||
this._lines += 1;
|
||||
relatedInformationNode.appendChild(container);
|
||||
}
|
||||
}
|
||||
|
||||
const fontInfo = this._editor.getOption(EditorOption.fontInfo);
|
||||
const scrollWidth = Math.ceil(fontInfo.typicalFullwidthCharacterWidth * this._longestLineLength * 0.75);
|
||||
const scrollHeight = fontInfo.lineHeight * this._lines;
|
||||
this._scrollable.setScrollDimensions({ scrollWidth, scrollHeight });
|
||||
}
|
||||
|
||||
layout(height: number, width: number): void {
|
||||
this._scrollable.getDomNode().style.height = `${height}px`;
|
||||
this._scrollable.getDomNode().style.width = `${width}px`;
|
||||
this._scrollable.setScrollDimensions({ width, height });
|
||||
}
|
||||
|
||||
getHeightInLines(): number {
|
||||
return Math.min(17, this._lines);
|
||||
}
|
||||
|
||||
private getAriaLabel(marker: IMarker): string {
|
||||
let severityLabel = '';
|
||||
switch (marker.severity) {
|
||||
case MarkerSeverity.Error:
|
||||
severityLabel = nls.localize('Error', "Error");
|
||||
break;
|
||||
case MarkerSeverity.Warning:
|
||||
severityLabel = nls.localize('Warning', "Warning");
|
||||
break;
|
||||
case MarkerSeverity.Info:
|
||||
severityLabel = nls.localize('Info', "Info");
|
||||
break;
|
||||
case MarkerSeverity.Hint:
|
||||
severityLabel = nls.localize('Hint', "Hint");
|
||||
break;
|
||||
}
|
||||
|
||||
let ariaLabel = nls.localize('marker aria', "{0} at {1}. ", severityLabel, marker.startLineNumber + ':' + marker.startColumn);
|
||||
const model = this._editor.getModel();
|
||||
if (model && (marker.startLineNumber <= model.getLineCount()) && (marker.startLineNumber >= 1)) {
|
||||
const lineContent = model.getLineContent(marker.startLineNumber);
|
||||
ariaLabel = `${lineContent}, ${ariaLabel}`;
|
||||
}
|
||||
return ariaLabel;
|
||||
}
|
||||
}
|
||||
|
||||
export class MarkerNavigationWidget extends PeekViewWidget {
|
||||
|
||||
static readonly TitleMenu = new MenuId('gotoErrorTitleMenu');
|
||||
|
||||
private _parentContainer!: HTMLElement;
|
||||
private _container!: HTMLElement;
|
||||
private _icon!: HTMLElement;
|
||||
private _message!: MessageWidget;
|
||||
private readonly _callOnDispose = new DisposableStore();
|
||||
private _severity: MarkerSeverity;
|
||||
private _backgroundColor?: Color;
|
||||
private readonly _onDidSelectRelatedInformation = new Emitter<IRelatedInformation>();
|
||||
private _heightInPixel!: number;
|
||||
|
||||
readonly onDidSelectRelatedInformation: Event<IRelatedInformation> = this._onDidSelectRelatedInformation.event;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@IThemeService private readonly _themeService: IThemeService,
|
||||
@IOpenerService private readonly _openerService: IOpenerService,
|
||||
@IMenuService private readonly _menuService: IMenuService,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService
|
||||
) {
|
||||
super(editor, { showArrow: true, showFrame: true, isAccessible: true }, instantiationService);
|
||||
this._severity = MarkerSeverity.Warning;
|
||||
this._backgroundColor = Color.white;
|
||||
|
||||
this._applyTheme(_themeService.getColorTheme());
|
||||
this._callOnDispose.add(_themeService.onDidColorThemeChange(this._applyTheme.bind(this)));
|
||||
|
||||
this.create();
|
||||
}
|
||||
|
||||
private _applyTheme(theme: IColorTheme) {
|
||||
this._backgroundColor = theme.getColor(editorMarkerNavigationBackground);
|
||||
let colorId = editorMarkerNavigationError;
|
||||
if (this._severity === MarkerSeverity.Warning) {
|
||||
colorId = editorMarkerNavigationWarning;
|
||||
} else if (this._severity === MarkerSeverity.Info) {
|
||||
colorId = editorMarkerNavigationInfo;
|
||||
}
|
||||
const frameColor = theme.getColor(colorId);
|
||||
this.style({
|
||||
arrowColor: frameColor,
|
||||
frameColor: frameColor,
|
||||
headerBackgroundColor: this._backgroundColor,
|
||||
primaryHeadingColor: theme.getColor(peekViewTitleForeground),
|
||||
secondaryHeadingColor: theme.getColor(peekViewTitleInfoForeground)
|
||||
}); // style() will trigger _applyStyles
|
||||
}
|
||||
|
||||
protected _applyStyles(): void {
|
||||
if (this._parentContainer) {
|
||||
this._parentContainer.style.backgroundColor = this._backgroundColor ? this._backgroundColor.toString() : '';
|
||||
}
|
||||
super._applyStyles();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._callOnDispose.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this._parentContainer.focus();
|
||||
}
|
||||
|
||||
protected _fillHead(container: HTMLElement): void {
|
||||
super._fillHead(container);
|
||||
|
||||
this._disposables.add(this._actionbarWidget!.actionRunner.onDidBeforeRun(e => this.editor.focus()));
|
||||
|
||||
const actions: IAction[] = [];
|
||||
const menu = this._menuService.createMenu(MarkerNavigationWidget.TitleMenu, this._contextKeyService);
|
||||
createAndFillInActionBarActions(menu, undefined, actions);
|
||||
this._actionbarWidget!.push(actions, { label: false, icon: true, index: 0 });
|
||||
menu.dispose();
|
||||
}
|
||||
|
||||
protected _fillTitleIcon(container: HTMLElement): void {
|
||||
this._icon = dom.append(container, dom.$(''));
|
||||
}
|
||||
|
||||
protected _getActionBarOptions(): IActionBarOptions {
|
||||
return {
|
||||
...super._getActionBarOptions(),
|
||||
orientation: ActionsOrientation.HORIZONTAL
|
||||
};
|
||||
}
|
||||
|
||||
protected _fillBody(container: HTMLElement): void {
|
||||
this._parentContainer = container;
|
||||
container.classList.add('marker-widget');
|
||||
this._parentContainer.tabIndex = 0;
|
||||
this._parentContainer.setAttribute('role', 'tooltip');
|
||||
|
||||
this._container = document.createElement('div');
|
||||
container.appendChild(this._container);
|
||||
|
||||
this._message = new MessageWidget(this._container, this.editor, related => this._onDidSelectRelatedInformation.fire(related), this._openerService);
|
||||
this._disposables.add(this._message);
|
||||
}
|
||||
|
||||
show(): void {
|
||||
throw new Error('call showAtMarker');
|
||||
}
|
||||
|
||||
showAtMarker(marker: IMarker, markerIdx: number, markerCount: number): void {
|
||||
// update:
|
||||
// * title
|
||||
// * message
|
||||
this._container.classList.remove('stale');
|
||||
this._message.update(marker);
|
||||
|
||||
// update frame color (only applied on 'show')
|
||||
this._severity = marker.severity;
|
||||
this._applyTheme(this._themeService.getColorTheme());
|
||||
|
||||
// show
|
||||
let range = Range.lift(marker);
|
||||
const editorPosition = this.editor.getPosition();
|
||||
let position = editorPosition && range.containsPosition(editorPosition) ? editorPosition : range.getStartPosition();
|
||||
super.show(position, this.computeRequiredHeight());
|
||||
|
||||
const model = this.editor.getModel();
|
||||
if (model) {
|
||||
const detail = markerCount > 1
|
||||
? nls.localize('problems', "{0} of {1} problems", markerIdx, markerCount)
|
||||
: nls.localize('change', "{0} of {1} problem", markerIdx, markerCount);
|
||||
this.setTitle(basename(model.uri), detail);
|
||||
}
|
||||
this._icon.className = `codicon ${SeverityIcon.className(MarkerSeverity.toSeverity(this._severity))}`;
|
||||
|
||||
this.editor.revealPositionNearTop(position, ScrollType.Smooth);
|
||||
this.editor.focus();
|
||||
}
|
||||
|
||||
updateMarker(marker: IMarker): void {
|
||||
this._container.classList.remove('stale');
|
||||
this._message.update(marker);
|
||||
}
|
||||
|
||||
showStale() {
|
||||
this._container.classList.add('stale');
|
||||
this._relayout();
|
||||
}
|
||||
|
||||
protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void {
|
||||
super._doLayoutBody(heightInPixel, widthInPixel);
|
||||
this._heightInPixel = heightInPixel;
|
||||
this._message.layout(heightInPixel, widthInPixel);
|
||||
this._container.style.height = `${heightInPixel}px`;
|
||||
}
|
||||
|
||||
public _onWidth(widthInPixel: number): void {
|
||||
this._message.layout(this._heightInPixel, widthInPixel);
|
||||
}
|
||||
|
||||
protected _relayout(): void {
|
||||
super._relayout(this.computeRequiredHeight());
|
||||
}
|
||||
|
||||
private computeRequiredHeight() {
|
||||
return 3 + this._message.getHeightInLines();
|
||||
}
|
||||
}
|
||||
|
||||
// theming
|
||||
|
||||
let errorDefault = oneOf(editorErrorForeground, editorErrorBorder);
|
||||
let warningDefault = oneOf(editorWarningForeground, editorWarningBorder);
|
||||
let infoDefault = oneOf(editorInfoForeground, editorInfoBorder);
|
||||
|
||||
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 editorMarkerNavigationInfo = registerColor('editorMarkerNavigationInfo.background', { dark: infoDefault, light: infoDefault, hc: infoDefault }, nls.localize('editorMarkerNavigationInfo', 'Editor marker navigation widget info color.'));
|
||||
export const editorMarkerNavigationBackground = registerColor('editorMarkerNavigation.background', { dark: '#2D2D30', light: Color.white, hc: '#0C141F' }, nls.localize('editorMarkerNavigationBackground', 'Editor marker navigation widget background.'));
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const linkFg = theme.getColor(textLinkForeground);
|
||||
if (linkFg) {
|
||||
collector.addRule(`.monaco-editor .marker-widget a { color: ${linkFg}; }`);
|
||||
collector.addRule(`.monaco-editor .marker-widget a.code-link span:hover { color: ${linkFg}; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,217 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IMarkerService, MarkerSeverity, IMarker } from 'vs/platform/markers/common/markers';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { compare } from 'vs/base/common/strings';
|
||||
import { binarySearch } from 'vs/base/common/arrays';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { LinkedList } from 'vs/base/common/linkedList';
|
||||
|
||||
export class MarkerCoordinate {
|
||||
constructor(
|
||||
readonly marker: IMarker,
|
||||
readonly index: number,
|
||||
readonly total: number
|
||||
) { }
|
||||
}
|
||||
|
||||
export class MarkerList {
|
||||
|
||||
private readonly _onDidChange = new Emitter<void>();
|
||||
readonly onDidChange: Event<void> = this._onDidChange.event;
|
||||
|
||||
private readonly _resourceFilter?: (uri: URI) => boolean;
|
||||
private readonly _dispoables = new DisposableStore();
|
||||
|
||||
private _markers: IMarker[] = [];
|
||||
private _nextIdx: number = -1;
|
||||
|
||||
constructor(
|
||||
resourceFilter: URI | ((uri: URI) => boolean) | undefined,
|
||||
@IMarkerService private readonly _markerService: IMarkerService,
|
||||
) {
|
||||
if (URI.isUri(resourceFilter)) {
|
||||
this._resourceFilter = uri => uri.toString() === resourceFilter.toString();
|
||||
} else if (resourceFilter) {
|
||||
this._resourceFilter = resourceFilter;
|
||||
}
|
||||
|
||||
const updateMarker = () => {
|
||||
this._markers = this._markerService.read({
|
||||
resource: URI.isUri(resourceFilter) ? resourceFilter : undefined,
|
||||
severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info
|
||||
});
|
||||
if (typeof resourceFilter === 'function') {
|
||||
this._markers = this._markers.filter(m => this._resourceFilter!(m.resource));
|
||||
}
|
||||
this._markers.sort(MarkerList._compareMarker);
|
||||
};
|
||||
|
||||
updateMarker();
|
||||
|
||||
this._dispoables.add(_markerService.onMarkerChanged(uris => {
|
||||
if (!this._resourceFilter || uris.some(uri => this._resourceFilter!(uri))) {
|
||||
updateMarker();
|
||||
this._nextIdx = -1;
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._dispoables.dispose();
|
||||
this._onDidChange.dispose();
|
||||
}
|
||||
|
||||
matches(uri: URI | undefined) {
|
||||
if (!this._resourceFilter && !uri) {
|
||||
return true;
|
||||
}
|
||||
if (!this._resourceFilter || !uri) {
|
||||
return false;
|
||||
}
|
||||
return this._resourceFilter(uri);
|
||||
}
|
||||
|
||||
get selected(): MarkerCoordinate | undefined {
|
||||
const marker = this._markers[this._nextIdx];
|
||||
return marker && new MarkerCoordinate(marker, this._nextIdx + 1, this._markers.length);
|
||||
}
|
||||
|
||||
private _initIdx(model: ITextModel, position: Position, fwd: boolean): void {
|
||||
let found = false;
|
||||
|
||||
let idx = this._markers.findIndex(marker => marker.resource.toString() === model.uri.toString());
|
||||
if (idx < 0) {
|
||||
idx = binarySearch(this._markers, <any>{ resource: model.uri }, (a, b) => compare(a.resource.toString(), b.resource.toString()));
|
||||
if (idx < 0) {
|
||||
idx = ~idx;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = idx; i < this._markers.length; i++) {
|
||||
let range = Range.lift(this._markers[i]);
|
||||
|
||||
if (range.isEmpty()) {
|
||||
const word = model.getWordAtPosition(range.getStartPosition());
|
||||
if (word) {
|
||||
range = new Range(range.startLineNumber, word.startColumn, range.startLineNumber, word.endColumn);
|
||||
}
|
||||
}
|
||||
|
||||
if (position && (range.containsPosition(position) || position.isBeforeOrEqual(range.getStartPosition()))) {
|
||||
this._nextIdx = i;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (this._markers[i].resource.toString() !== model.uri.toString()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
resetIndex() {
|
||||
this._nextIdx = -1;
|
||||
}
|
||||
|
||||
move(fwd: boolean, model: ITextModel, position: Position): boolean {
|
||||
if (this._markers.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let oldIdx = this._nextIdx;
|
||||
if (this._nextIdx === -1) {
|
||||
this._initIdx(model, position, fwd);
|
||||
} else if (fwd) {
|
||||
this._nextIdx = (this._nextIdx + 1) % this._markers.length;
|
||||
} else if (!fwd) {
|
||||
this._nextIdx = (this._nextIdx - 1 + this._markers.length) % this._markers.length;
|
||||
}
|
||||
|
||||
if (oldIdx !== this._nextIdx) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
find(uri: URI, position: Position): MarkerCoordinate | undefined {
|
||||
let idx = this._markers.findIndex(marker => marker.resource.toString() === uri.toString());
|
||||
if (idx < 0) {
|
||||
return undefined;
|
||||
}
|
||||
for (; idx < this._markers.length; idx++) {
|
||||
if (Range.containsPosition(this._markers[idx], position)) {
|
||||
return new MarkerCoordinate(this._markers[idx], idx + 1, this._markers.length);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private static _compareMarker(a: IMarker, b: IMarker): number {
|
||||
let res = compare(a.resource.toString(), b.resource.toString());
|
||||
if (res === 0) {
|
||||
res = MarkerSeverity.compare(a.severity, b.severity);
|
||||
}
|
||||
if (res === 0) {
|
||||
res = Range.compareRangesUsingStarts(a, b);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export const IMarkerNavigationService = createDecorator<IMarkerNavigationService>('IMarkerNavigationService');
|
||||
|
||||
export interface IMarkerNavigationService {
|
||||
readonly _serviceBrand: undefined;
|
||||
registerProvider(provider: IMarkerListProvider): IDisposable;
|
||||
getMarkerList(resource: URI | undefined): MarkerList;
|
||||
}
|
||||
|
||||
export interface IMarkerListProvider {
|
||||
getMarkerList(resource: URI | undefined): MarkerList | undefined;
|
||||
}
|
||||
|
||||
class MarkerNavigationService implements IMarkerNavigationService, IMarkerListProvider {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _provider = new LinkedList<IMarkerListProvider>();
|
||||
|
||||
constructor(@IMarkerService private readonly _markerService: IMarkerService) { }
|
||||
|
||||
registerProvider(provider: IMarkerListProvider): IDisposable {
|
||||
const remove = this._provider.unshift(provider);
|
||||
return toDisposable(() => remove());
|
||||
}
|
||||
|
||||
getMarkerList(resource: URI | undefined): MarkerList {
|
||||
for (let provider of this._provider) {
|
||||
const result = provider.getMarkerList(resource);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
// default
|
||||
return new MarkerList(resource, this._markerService);
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IMarkerNavigationService, MarkerNavigationService, true);
|
||||
@@ -0,0 +1,71 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* marker zone */
|
||||
|
||||
.monaco-editor .peekview-widget .head .peekview-title .severity-icon {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget > .stale {
|
||||
opacity: 0.6;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget .title {
|
||||
display: inline-block;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget .descriptioncontainer {
|
||||
position: absolute;
|
||||
white-space: pre;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
padding: 8px 12px 0 20px;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget .descriptioncontainer .message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget .descriptioncontainer .message .details {
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget .descriptioncontainer .message .source,
|
||||
.monaco-editor .marker-widget .descriptioncontainer .message span.code {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget .descriptioncontainer .message a.code-link {
|
||||
opacity: 0.6;
|
||||
color: inherit;
|
||||
}
|
||||
.monaco-editor .marker-widget .descriptioncontainer .message a.code-link:before {
|
||||
content: '(';
|
||||
}
|
||||
.monaco-editor .marker-widget .descriptioncontainer .message a.code-link:after {
|
||||
content: ')';
|
||||
}
|
||||
.monaco-editor .marker-widget .descriptioncontainer .message a.code-link > span {
|
||||
text-decoration: underline;
|
||||
/** Hack to force underline to show **/
|
||||
border-bottom: 1px solid transparent;
|
||||
text-underline-position: under;
|
||||
}
|
||||
|
||||
.monaco-editor .marker-widget .descriptioncontainer .filename {
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { DocumentSymbol } from 'vs/editor/common/modes';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { assertType } from 'vs/base/common/types';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
|
||||
export async function getDocumentSymbols(document: ITextModel, flat: boolean, token: CancellationToken): Promise<DocumentSymbol[]> {
|
||||
|
||||
const model = await OutlineModel.create(document, token);
|
||||
const roots: DocumentSymbol[] = [];
|
||||
for (const child of model.children.values()) {
|
||||
if (child instanceof OutlineElement) {
|
||||
roots.push(child.symbol);
|
||||
} else {
|
||||
roots.push(...Iterable.map(child.children.values(), child => child.symbol));
|
||||
}
|
||||
}
|
||||
|
||||
let flatEntries: DocumentSymbol[] = [];
|
||||
if (token.isCancellationRequested) {
|
||||
return flatEntries;
|
||||
}
|
||||
if (flat) {
|
||||
flatten(flatEntries, roots, '');
|
||||
} else {
|
||||
flatEntries = roots;
|
||||
}
|
||||
|
||||
return flatEntries.sort(compareEntriesUsingStart);
|
||||
}
|
||||
|
||||
function compareEntriesUsingStart(a: DocumentSymbol, b: DocumentSymbol): number {
|
||||
return Range.compareRangesUsingStarts(a.range, b.range);
|
||||
}
|
||||
|
||||
function flatten(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void {
|
||||
for (let entry of entries) {
|
||||
bucket.push({
|
||||
kind: entry.kind,
|
||||
tags: entry.tags,
|
||||
name: entry.name,
|
||||
detail: entry.detail,
|
||||
containerName: entry.containerName || overrideContainerLabel,
|
||||
range: entry.range,
|
||||
selectionRange: entry.selectionRange,
|
||||
children: undefined, // we flatten it...
|
||||
});
|
||||
if (entry.children) {
|
||||
flatten(bucket, entry.children, entry.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CommandsRegistry.registerCommand('_executeDocumentSymbolProvider', async function (accessor, ...args) {
|
||||
const [resource] = args;
|
||||
assertType(URI.isUri(resource));
|
||||
|
||||
const model = accessor.get(IModelService).getModel(resource);
|
||||
if (model) {
|
||||
return getDocumentSymbols(model, false, CancellationToken.None);
|
||||
}
|
||||
|
||||
const reference = await accessor.get(ITextModelService).createModelReference(resource);
|
||||
try {
|
||||
return await getDocumentSymbols(reference.object.textEditorModel, false, CancellationToken.None);
|
||||
} finally {
|
||||
reference.dispose();
|
||||
}
|
||||
});
|
||||
807
lib/vscode/src/vs/editor/contrib/gotoSymbol/goToCommands.ts
Normal file
807
lib/vscode/src/vs/editor/contrib/gotoSymbol/goToCommands.ts
Normal file
@@ -0,0 +1,807 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { alert } from 'vs/base/browser/ui/aria/aria';
|
||||
import { createCancelablePromise, raceCancellation } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { ICodeEditor, isCodeEditor, IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, IActionOptions, registerEditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import * as corePosition from 'vs/editor/common/core/position';
|
||||
import { Range, IRange } from 'vs/editor/common/core/range';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { ITextModel, IWordAtPosition } from 'vs/editor/common/model';
|
||||
import { LocationLink, Location, isLocationLink } from 'vs/editor/common/modes';
|
||||
import { MessageController } from 'vs/editor/contrib/message/messageController';
|
||||
import { PeekContext } from 'vs/editor/contrib/peekView/peekView';
|
||||
import { ReferencesController } from 'vs/editor/contrib/gotoSymbol/peek/referencesController';
|
||||
import { ReferencesModel } from 'vs/editor/contrib/gotoSymbol/referencesModel';
|
||||
import * as nls from 'vs/nls';
|
||||
import { MenuId, MenuRegistry, ISubmenuItem } from 'vs/platform/actions/common/actions';
|
||||
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { IEditorProgressService } from 'vs/platform/progress/common/progress';
|
||||
import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition, getDeclarationsAtPosition, getReferencesAtPosition } from './goToSymbol';
|
||||
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { EditorStateCancellationTokenSource, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState';
|
||||
import { ISymbolNavigationService } from 'vs/editor/contrib/gotoSymbol/symbolNavigation';
|
||||
import { EditorOption, GoToLocationValues } from 'vs/editor/common/config/editorOptions';
|
||||
import { isStandalone } from 'vs/base/browser/browser';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ScrollType, IEditorAction } from 'vs/editor/common/editorCommon';
|
||||
import { assertType } from 'vs/base/common/types';
|
||||
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
||||
import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor';
|
||||
|
||||
|
||||
MenuRegistry.appendMenuItem(MenuId.EditorContext, <ISubmenuItem>{
|
||||
submenu: MenuId.EditorContextPeek,
|
||||
title: nls.localize('peek.submenu', "Peek"),
|
||||
group: 'navigation',
|
||||
order: 100
|
||||
});
|
||||
|
||||
export interface SymbolNavigationActionConfig {
|
||||
openToSide: boolean;
|
||||
openInPeek: boolean;
|
||||
muteMessage: boolean;
|
||||
}
|
||||
|
||||
abstract class SymbolNavigationAction extends EditorAction {
|
||||
|
||||
private readonly _configuration: SymbolNavigationActionConfig;
|
||||
|
||||
constructor(configuration: SymbolNavigationActionConfig, opts: IActionOptions) {
|
||||
super(opts);
|
||||
this._configuration = configuration;
|
||||
}
|
||||
|
||||
run(accessor: ServicesAccessor, editor: ICodeEditor): Promise<void> {
|
||||
if (!editor.hasModel()) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
const notificationService = accessor.get(INotificationService);
|
||||
const editorService = accessor.get(ICodeEditorService);
|
||||
const progressService = accessor.get(IEditorProgressService);
|
||||
const symbolNavService = accessor.get(ISymbolNavigationService);
|
||||
|
||||
const model = editor.getModel();
|
||||
const pos = editor.getPosition();
|
||||
|
||||
const cts = new EditorStateCancellationTokenSource(editor, CodeEditorStateFlag.Value | CodeEditorStateFlag.Position);
|
||||
|
||||
const promise = raceCancellation(this._getLocationModel(model, pos, cts.token), cts.token).then(async references => {
|
||||
|
||||
if (!references || cts.token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
alert(references.ariaMessage);
|
||||
|
||||
let altAction: IEditorAction | null | undefined;
|
||||
if (references.referenceAt(model.uri, pos)) {
|
||||
const altActionId = this._getAlternativeCommand(editor);
|
||||
if (altActionId !== this.id) {
|
||||
altAction = editor.getAction(altActionId);
|
||||
}
|
||||
}
|
||||
|
||||
const referenceCount = references.references.length;
|
||||
|
||||
if (referenceCount === 0) {
|
||||
// no result -> show message
|
||||
if (!this._configuration.muteMessage) {
|
||||
const info = model.getWordAtPosition(pos);
|
||||
MessageController.get(editor).showMessage(this._getNoResultFoundMessage(info), pos);
|
||||
}
|
||||
} else if (referenceCount === 1 && altAction) {
|
||||
// already at the only result, run alternative
|
||||
altAction.run();
|
||||
|
||||
} else {
|
||||
// normal results handling
|
||||
return this._onResult(editorService, symbolNavService, editor, references);
|
||||
}
|
||||
|
||||
}, (err) => {
|
||||
// report an error
|
||||
notificationService.error(err);
|
||||
}).finally(() => {
|
||||
cts.dispose();
|
||||
});
|
||||
|
||||
progressService.showWhile(promise, 250);
|
||||
return promise;
|
||||
}
|
||||
|
||||
protected abstract _getLocationModel(model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise<ReferencesModel | undefined>;
|
||||
|
||||
protected abstract _getNoResultFoundMessage(info: IWordAtPosition | null): string;
|
||||
|
||||
protected abstract _getAlternativeCommand(editor: IActiveCodeEditor): string;
|
||||
|
||||
protected abstract _getGoToPreference(editor: IActiveCodeEditor): GoToLocationValues;
|
||||
|
||||
private async _onResult(editorService: ICodeEditorService, symbolNavService: ISymbolNavigationService, editor: IActiveCodeEditor, model: ReferencesModel): Promise<void> {
|
||||
|
||||
const gotoLocation = this._getGoToPreference(editor);
|
||||
if (!(editor instanceof EmbeddedCodeEditorWidget) && (this._configuration.openInPeek || (gotoLocation === 'peek' && model.references.length > 1))) {
|
||||
this._openInPeek(editor, model);
|
||||
|
||||
} else {
|
||||
const next = model.firstReference()!;
|
||||
const peek = model.references.length > 1 && gotoLocation === 'gotoAndPeek';
|
||||
const targetEditor = await this._openReference(editor, editorService, next, this._configuration.openToSide, !peek);
|
||||
if (peek && targetEditor) {
|
||||
this._openInPeek(targetEditor, model);
|
||||
} else {
|
||||
model.dispose();
|
||||
}
|
||||
|
||||
// keep remaining locations around when using
|
||||
// 'goto'-mode
|
||||
if (gotoLocation === 'goto') {
|
||||
symbolNavService.put(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _openReference(editor: ICodeEditor, editorService: ICodeEditorService, reference: Location | LocationLink, sideBySide: boolean, highlight: boolean): Promise<ICodeEditor | undefined> {
|
||||
// range is the target-selection-range when we have one
|
||||
// and the fallback is the 'full' range
|
||||
let range: IRange | undefined = undefined;
|
||||
if (isLocationLink(reference)) {
|
||||
range = reference.targetSelectionRange;
|
||||
}
|
||||
if (!range) {
|
||||
range = reference.range;
|
||||
}
|
||||
|
||||
const targetEditor = await editorService.openCodeEditor({
|
||||
resource: reference.uri,
|
||||
options: {
|
||||
selection: Range.collapseToStart(range),
|
||||
selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport
|
||||
}
|
||||
}, editor, sideBySide);
|
||||
|
||||
if (!targetEditor) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (highlight) {
|
||||
const modelNow = targetEditor.getModel();
|
||||
const ids = targetEditor.deltaDecorations([], [{ range, options: { className: 'symbolHighlight' } }]);
|
||||
setTimeout(() => {
|
||||
if (targetEditor.getModel() === modelNow) {
|
||||
targetEditor.deltaDecorations(ids, []);
|
||||
}
|
||||
}, 350);
|
||||
}
|
||||
|
||||
return targetEditor;
|
||||
}
|
||||
|
||||
private _openInPeek(target: ICodeEditor, model: ReferencesModel) {
|
||||
let controller = ReferencesController.get(target);
|
||||
if (controller && target.hasModel()) {
|
||||
controller.toggleWidget(target.getSelection(), createCancelablePromise(_ => Promise.resolve(model)), this._configuration.openInPeek);
|
||||
} else {
|
||||
model.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//#region --- DEFINITION
|
||||
|
||||
export class DefinitionAction extends SymbolNavigationAction {
|
||||
|
||||
protected async _getLocationModel(model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise<ReferencesModel> {
|
||||
return new ReferencesModel(await getDefinitionsAtPosition(model, position, token), nls.localize('def.title', 'Definitions'));
|
||||
}
|
||||
|
||||
protected _getNoResultFoundMessage(info: IWordAtPosition | null): string {
|
||||
return info && info.word
|
||||
? nls.localize('noResultWord', "No definition found for '{0}'", info.word)
|
||||
: nls.localize('generic.noResults', "No definition found");
|
||||
}
|
||||
|
||||
protected _getAlternativeCommand(editor: IActiveCodeEditor): string {
|
||||
return editor.getOption(EditorOption.gotoLocation).alternativeDefinitionCommand;
|
||||
}
|
||||
|
||||
protected _getGoToPreference(editor: IActiveCodeEditor): GoToLocationValues {
|
||||
return editor.getOption(EditorOption.gotoLocation).multipleDefinitions;
|
||||
}
|
||||
}
|
||||
|
||||
const goToDefinitionKb = isWeb && !isStandalone
|
||||
? KeyMod.CtrlCmd | KeyCode.F12
|
||||
: KeyCode.F12;
|
||||
|
||||
registerEditorAction(class GoToDefinitionAction extends DefinitionAction {
|
||||
|
||||
static readonly id = 'editor.action.revealDefinition';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: false,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: GoToDefinitionAction.id,
|
||||
label: nls.localize('actions.goToDecl.label', "Go to Definition"),
|
||||
alias: 'Go to Definition',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasDefinitionProvider,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: goToDefinitionKb,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
contextMenuOpts: {
|
||||
group: 'navigation',
|
||||
order: 1.1
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarGoMenu,
|
||||
group: '4_symbol_nav',
|
||||
order: 2,
|
||||
title: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition")
|
||||
}
|
||||
});
|
||||
CommandsRegistry.registerCommandAlias('editor.action.goToDeclaration', GoToDefinitionAction.id);
|
||||
}
|
||||
});
|
||||
|
||||
registerEditorAction(class OpenDefinitionToSideAction extends DefinitionAction {
|
||||
|
||||
static readonly id = 'editor.action.revealDefinitionAside';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: true,
|
||||
openInPeek: false,
|
||||
muteMessage: false
|
||||
}, {
|
||||
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.isInWalkThroughSnippet.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, goToDefinitionKb),
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
}
|
||||
});
|
||||
CommandsRegistry.registerCommandAlias('editor.action.openDeclarationToTheSide', OpenDefinitionToSideAction.id);
|
||||
}
|
||||
});
|
||||
|
||||
registerEditorAction(class PeekDefinitionAction extends DefinitionAction {
|
||||
|
||||
static readonly id = 'editor.action.peekDefinition';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: true,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: PeekDefinitionAction.id,
|
||||
label: nls.localize('actions.previewDecl.label', "Peek Definition"),
|
||||
alias: 'Peek Definition',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasDefinitionProvider,
|
||||
PeekContext.notInPeekEditor,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()
|
||||
),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.Alt | KeyCode.F12,
|
||||
linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F10 },
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
contextMenuOpts: {
|
||||
menuId: MenuId.EditorContextPeek,
|
||||
group: 'peek',
|
||||
order: 2
|
||||
}
|
||||
});
|
||||
CommandsRegistry.registerCommandAlias('editor.action.previewDeclaration', PeekDefinitionAction.id);
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region --- DECLARATION
|
||||
|
||||
class DeclarationAction extends SymbolNavigationAction {
|
||||
|
||||
protected async _getLocationModel(model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise<ReferencesModel> {
|
||||
return new ReferencesModel(await getDeclarationsAtPosition(model, position, token), nls.localize('decl.title', 'Declarations'));
|
||||
}
|
||||
|
||||
protected _getNoResultFoundMessage(info: IWordAtPosition | null): string {
|
||||
return info && info.word
|
||||
? nls.localize('decl.noResultWord', "No declaration found for '{0}'", info.word)
|
||||
: nls.localize('decl.generic.noResults', "No declaration found");
|
||||
}
|
||||
|
||||
protected _getAlternativeCommand(editor: IActiveCodeEditor): string {
|
||||
return editor.getOption(EditorOption.gotoLocation).alternativeDeclarationCommand;
|
||||
}
|
||||
|
||||
protected _getGoToPreference(editor: IActiveCodeEditor): GoToLocationValues {
|
||||
return editor.getOption(EditorOption.gotoLocation).multipleDeclarations;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(class GoToDeclarationAction extends DeclarationAction {
|
||||
|
||||
static readonly id = 'editor.action.revealDeclaration';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: false,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: GoToDeclarationAction.id,
|
||||
label: nls.localize('actions.goToDeclaration.label', "Go to Declaration"),
|
||||
alias: 'Go to Declaration',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasDeclarationProvider,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()
|
||||
),
|
||||
contextMenuOpts: {
|
||||
group: 'navigation',
|
||||
order: 1.3
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarGoMenu,
|
||||
group: '4_symbol_nav',
|
||||
order: 3,
|
||||
title: nls.localize({ key: 'miGotoDeclaration', comment: ['&& denotes a mnemonic'] }, "Go to &&Declaration")
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected _getNoResultFoundMessage(info: IWordAtPosition | null): string {
|
||||
return info && info.word
|
||||
? nls.localize('decl.noResultWord', "No declaration found for '{0}'", info.word)
|
||||
: nls.localize('decl.generic.noResults', "No declaration found");
|
||||
}
|
||||
});
|
||||
|
||||
registerEditorAction(class PeekDeclarationAction extends DeclarationAction {
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: true,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: 'editor.action.peekDeclaration',
|
||||
label: nls.localize('actions.peekDecl.label', "Peek Declaration"),
|
||||
alias: 'Peek Declaration',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasDeclarationProvider,
|
||||
PeekContext.notInPeekEditor,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()
|
||||
),
|
||||
contextMenuOpts: {
|
||||
menuId: MenuId.EditorContextPeek,
|
||||
group: 'peek',
|
||||
order: 3
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region --- TYPE DEFINITION
|
||||
|
||||
class TypeDefinitionAction extends SymbolNavigationAction {
|
||||
|
||||
protected async _getLocationModel(model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise<ReferencesModel> {
|
||||
return new ReferencesModel(await getTypeDefinitionsAtPosition(model, position, token), nls.localize('typedef.title', 'Type Definitions'));
|
||||
}
|
||||
|
||||
protected _getNoResultFoundMessage(info: IWordAtPosition | null): 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 _getAlternativeCommand(editor: IActiveCodeEditor): string {
|
||||
return editor.getOption(EditorOption.gotoLocation).alternativeTypeDefinitionCommand;
|
||||
}
|
||||
|
||||
protected _getGoToPreference(editor: IActiveCodeEditor): GoToLocationValues {
|
||||
return editor.getOption(EditorOption.gotoLocation).multipleTypeDefinitions;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(class GoToTypeDefinitionAction extends TypeDefinitionAction {
|
||||
|
||||
public static readonly ID = 'editor.action.goToTypeDefinition';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: false,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: GoToTypeDefinitionAction.ID,
|
||||
label: nls.localize('actions.goToTypeDefinition.label', "Go to Type Definition"),
|
||||
alias: 'Go to Type Definition',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasTypeDefinitionProvider,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: 0,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
contextMenuOpts: {
|
||||
group: 'navigation',
|
||||
order: 1.4
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarGoMenu,
|
||||
group: '4_symbol_nav',
|
||||
order: 3,
|
||||
title: nls.localize({ key: 'miGotoTypeDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Type Definition")
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
registerEditorAction(class PeekTypeDefinitionAction extends TypeDefinitionAction {
|
||||
|
||||
public static readonly ID = 'editor.action.peekTypeDefinition';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: true,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: PeekTypeDefinitionAction.ID,
|
||||
label: nls.localize('actions.peekTypeDefinition.label', "Peek Type Definition"),
|
||||
alias: 'Peek Type Definition',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasTypeDefinitionProvider,
|
||||
PeekContext.notInPeekEditor,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()
|
||||
),
|
||||
contextMenuOpts: {
|
||||
menuId: MenuId.EditorContextPeek,
|
||||
group: 'peek',
|
||||
order: 4
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region --- IMPLEMENTATION
|
||||
|
||||
class ImplementationAction extends SymbolNavigationAction {
|
||||
|
||||
protected async _getLocationModel(model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise<ReferencesModel> {
|
||||
return new ReferencesModel(await getImplementationsAtPosition(model, position, token), nls.localize('impl.title', 'Implementations'));
|
||||
}
|
||||
|
||||
protected _getNoResultFoundMessage(info: IWordAtPosition | null): 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 _getAlternativeCommand(editor: IActiveCodeEditor): string {
|
||||
return editor.getOption(EditorOption.gotoLocation).alternativeImplementationCommand;
|
||||
}
|
||||
|
||||
protected _getGoToPreference(editor: IActiveCodeEditor): GoToLocationValues {
|
||||
return editor.getOption(EditorOption.gotoLocation).multipleImplementations;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(class GoToImplementationAction extends ImplementationAction {
|
||||
|
||||
public static readonly ID = 'editor.action.goToImplementation';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: false,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: GoToImplementationAction.ID,
|
||||
label: nls.localize('actions.goToImplementation.label', "Go to Implementations"),
|
||||
alias: 'Go to Implementations',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasImplementationProvider,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.F12,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarGoMenu,
|
||||
group: '4_symbol_nav',
|
||||
order: 4,
|
||||
title: nls.localize({ key: 'miGotoImplementation', comment: ['&& denotes a mnemonic'] }, "Go to &&Implementations")
|
||||
},
|
||||
contextMenuOpts: {
|
||||
group: 'navigation',
|
||||
order: 1.45
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
registerEditorAction(class PeekImplementationAction extends ImplementationAction {
|
||||
|
||||
public static readonly ID = 'editor.action.peekImplementation';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: true,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: PeekImplementationAction.ID,
|
||||
label: nls.localize('actions.peekImplementation.label', "Peek Implementations"),
|
||||
alias: 'Peek Implementations',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasImplementationProvider,
|
||||
PeekContext.notInPeekEditor,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()
|
||||
),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F12,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
contextMenuOpts: {
|
||||
menuId: MenuId.EditorContextPeek,
|
||||
group: 'peek',
|
||||
order: 5
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region --- REFERENCES
|
||||
|
||||
abstract class ReferencesAction extends SymbolNavigationAction {
|
||||
|
||||
protected _getNoResultFoundMessage(info: IWordAtPosition | null): string {
|
||||
return info
|
||||
? nls.localize('references.no', "No references found for '{0}'", info.word)
|
||||
: nls.localize('references.noGeneric', "No references found");
|
||||
}
|
||||
|
||||
protected _getAlternativeCommand(editor: IActiveCodeEditor): string {
|
||||
return editor.getOption(EditorOption.gotoLocation).alternativeReferenceCommand;
|
||||
}
|
||||
|
||||
protected _getGoToPreference(editor: IActiveCodeEditor): GoToLocationValues {
|
||||
return editor.getOption(EditorOption.gotoLocation).multipleReferences;
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorAction(class GoToReferencesAction extends ReferencesAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: false,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: 'editor.action.goToReferences',
|
||||
label: nls.localize('goToReferences.label', "Go to References"),
|
||||
alias: 'Go to References',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasReferenceProvider,
|
||||
PeekContext.notInPeekEditor,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()
|
||||
),
|
||||
kbOpts: {
|
||||
kbExpr: EditorContextKeys.editorTextFocus,
|
||||
primary: KeyMod.Shift | KeyCode.F12,
|
||||
weight: KeybindingWeight.EditorContrib
|
||||
},
|
||||
contextMenuOpts: {
|
||||
group: 'navigation',
|
||||
order: 1.45
|
||||
},
|
||||
menuOpts: {
|
||||
menuId: MenuId.MenubarGoMenu,
|
||||
group: '4_symbol_nav',
|
||||
order: 5,
|
||||
title: nls.localize({ key: 'miGotoReference', comment: ['&& denotes a mnemonic'] }, "Go to &&References")
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
protected async _getLocationModel(model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise<ReferencesModel> {
|
||||
return new ReferencesModel(await getReferencesAtPosition(model, position, true, token), nls.localize('ref.title', 'References'));
|
||||
}
|
||||
});
|
||||
|
||||
registerEditorAction(class PeekReferencesAction extends ReferencesAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
openToSide: false,
|
||||
openInPeek: true,
|
||||
muteMessage: false
|
||||
}, {
|
||||
id: 'editor.action.referenceSearch.trigger',
|
||||
label: nls.localize('references.action.label', "Peek References"),
|
||||
alias: 'Peek References',
|
||||
precondition: ContextKeyExpr.and(
|
||||
EditorContextKeys.hasReferenceProvider,
|
||||
PeekContext.notInPeekEditor,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()
|
||||
),
|
||||
contextMenuOpts: {
|
||||
menuId: MenuId.EditorContextPeek,
|
||||
group: 'peek',
|
||||
order: 6
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async _getLocationModel(model: ITextModel, position: corePosition.Position, token: CancellationToken): Promise<ReferencesModel> {
|
||||
return new ReferencesModel(await getReferencesAtPosition(model, position, false, token), nls.localize('ref.title', 'References'));
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region --- GENERIC goto symbols command
|
||||
|
||||
class GenericGoToLocationAction extends SymbolNavigationAction {
|
||||
|
||||
constructor(
|
||||
config: SymbolNavigationActionConfig,
|
||||
private readonly _references: Location[],
|
||||
private readonly _gotoMultipleBehaviour: GoToLocationValues | undefined,
|
||||
) {
|
||||
super(config, {
|
||||
id: 'editor.action.goToLocation',
|
||||
label: nls.localize('label.generic', "Go To Any Symbol"),
|
||||
alias: 'Go To Any Symbol',
|
||||
precondition: ContextKeyExpr.and(
|
||||
PeekContext.notInPeekEditor,
|
||||
EditorContextKeys.isInWalkThroughSnippet.toNegated()
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
protected async _getLocationModel(_model: ITextModel, _position: corePosition.Position, _token: CancellationToken): Promise<ReferencesModel | undefined> {
|
||||
return new ReferencesModel(this._references, nls.localize('generic.title', 'Locations'));
|
||||
}
|
||||
|
||||
protected _getNoResultFoundMessage(info: IWordAtPosition | null): string {
|
||||
return info && nls.localize('generic.noResult', "No results for '{0}'", info.word) || '';
|
||||
}
|
||||
|
||||
protected _getGoToPreference(editor: IActiveCodeEditor): GoToLocationValues {
|
||||
return this._gotoMultipleBehaviour ?? editor.getOption(EditorOption.gotoLocation).multipleReferences;
|
||||
}
|
||||
|
||||
protected _getAlternativeCommand() { return ''; }
|
||||
}
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: 'editor.action.goToLocations',
|
||||
description: {
|
||||
description: 'Go to locations from a position in a file',
|
||||
args: [
|
||||
{ name: 'uri', description: 'The text document in which to start', constraint: URI },
|
||||
{ name: 'position', description: 'The position at which to start', constraint: corePosition.Position.isIPosition },
|
||||
{ name: 'locations', description: 'An array of locations.', constraint: Array },
|
||||
{ name: 'multiple', description: 'Define what to do when having multiple results, either `peek`, `gotoAndPeek`, or `goto' },
|
||||
{ name: 'noResultsMessage', description: 'Human readable message that shows when locations is empty.' },
|
||||
]
|
||||
},
|
||||
handler: async (accessor: ServicesAccessor, resource: any, position: any, references: any, multiple?: any, noResultsMessage?: string, openInPeek?: boolean) => {
|
||||
assertType(URI.isUri(resource));
|
||||
assertType(corePosition.Position.isIPosition(position));
|
||||
assertType(Array.isArray(references));
|
||||
assertType(typeof multiple === 'undefined' || typeof multiple === 'string');
|
||||
assertType(typeof openInPeek === 'undefined' || typeof openInPeek === 'boolean');
|
||||
|
||||
const editorService = accessor.get(ICodeEditorService);
|
||||
const editor = await editorService.openCodeEditor({ resource }, editorService.getFocusedCodeEditor());
|
||||
|
||||
if (isCodeEditor(editor)) {
|
||||
editor.setPosition(position);
|
||||
editor.revealPositionInCenterIfOutsideViewport(position, ScrollType.Smooth);
|
||||
|
||||
return editor.invokeWithinContext(accessor => {
|
||||
const command = new class extends GenericGoToLocationAction {
|
||||
_getNoResultFoundMessage(info: IWordAtPosition | null) {
|
||||
return noResultsMessage || super._getNoResultFoundMessage(info);
|
||||
}
|
||||
}({
|
||||
muteMessage: !Boolean(noResultsMessage),
|
||||
openInPeek: Boolean(openInPeek),
|
||||
openToSide: false
|
||||
}, references, multiple as GoToLocationValues);
|
||||
|
||||
accessor.get(IInstantiationService).invokeFunction(command.run.bind(command), editor);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: 'editor.action.peekLocations',
|
||||
description: {
|
||||
description: 'Peek locations from a position in a file',
|
||||
args: [
|
||||
{ name: 'uri', description: 'The text document in which to start', constraint: URI },
|
||||
{ name: 'position', description: 'The position at which to start', constraint: corePosition.Position.isIPosition },
|
||||
{ name: 'locations', description: 'An array of locations.', constraint: Array },
|
||||
{ name: 'multiple', description: 'Define what to do when having multiple results, either `peek`, `gotoAndPeek`, or `goto' },
|
||||
]
|
||||
},
|
||||
handler: async (accessor: ServicesAccessor, resource: any, position: any, references: any, multiple?: any) => {
|
||||
accessor.get(ICommandService).executeCommand('editor.action.goToLocations', resource, position, references, multiple, undefined, true);
|
||||
}
|
||||
});
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region --- REFERENCE search special commands
|
||||
|
||||
CommandsRegistry.registerCommand({
|
||||
id: 'editor.action.findReferences',
|
||||
handler: (accessor: ServicesAccessor, resource: any, position: any) => {
|
||||
assertType(URI.isUri(resource));
|
||||
assertType(corePosition.Position.isIPosition(position));
|
||||
|
||||
const codeEditorService = accessor.get(ICodeEditorService);
|
||||
return codeEditorService.openCodeEditor({ resource }, codeEditorService.getFocusedCodeEditor()).then(control => {
|
||||
if (!isCodeEditor(control) || !control.hasModel()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const controller = ReferencesController.get(control);
|
||||
if (!controller) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const references = createCancelablePromise(token => getReferencesAtPosition(control.getModel(), corePosition.Position.lift(position), false, token).then(references => new ReferencesModel(references, nls.localize('ref.title', 'References'))));
|
||||
const range = new Range(position.lineNumber, position.column, position.lineNumber, position.column);
|
||||
return Promise.resolve(controller.toggleWidget(range, references, false));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// use NEW command
|
||||
CommandsRegistry.registerCommandAlias('editor.action.showReferences', 'editor.action.peekLocations');
|
||||
|
||||
//#endregion
|
||||
87
lib/vscode/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts
Normal file
87
lib/vscode/src/vs/editor/contrib/gotoSymbol/goToSymbol.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { registerModelAndPositionCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { LocationLink, DefinitionProviderRegistry, ImplementationProviderRegistry, TypeDefinitionProviderRegistry, DeclarationProviderRegistry, ProviderResult, ReferenceProviderRegistry } from 'vs/editor/common/modes';
|
||||
import { LanguageFeatureRegistry } from 'vs/editor/common/modes/languageFeatureRegistry';
|
||||
|
||||
|
||||
function getLocationLinks<T>(
|
||||
model: ITextModel,
|
||||
position: Position,
|
||||
registry: LanguageFeatureRegistry<T>,
|
||||
provide: (provider: T, model: ITextModel, position: Position) => ProviderResult<LocationLink | LocationLink[]>
|
||||
): Promise<LocationLink[]> {
|
||||
const provider = registry.ordered(model);
|
||||
|
||||
// get results
|
||||
const promises = provider.map((provider): Promise<LocationLink | LocationLink[] | undefined> => {
|
||||
return Promise.resolve(provide(provider, model, position)).then(undefined, err => {
|
||||
onUnexpectedExternalError(err);
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(values => {
|
||||
const result: LocationLink[] = [];
|
||||
for (let value of values) {
|
||||
if (Array.isArray(value)) {
|
||||
result.push(...value);
|
||||
} else if (value) {
|
||||
result.push(value);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export function getDefinitionsAtPosition(model: ITextModel, position: Position, token: CancellationToken): Promise<LocationLink[]> {
|
||||
return getLocationLinks(model, position, DefinitionProviderRegistry, (provider, model, position) => {
|
||||
return provider.provideDefinition(model, position, token);
|
||||
});
|
||||
}
|
||||
|
||||
export function getDeclarationsAtPosition(model: ITextModel, position: Position, token: CancellationToken): Promise<LocationLink[]> {
|
||||
return getLocationLinks(model, position, DeclarationProviderRegistry, (provider, model, position) => {
|
||||
return provider.provideDeclaration(model, position, token);
|
||||
});
|
||||
}
|
||||
|
||||
export function getImplementationsAtPosition(model: ITextModel, position: Position, token: CancellationToken): Promise<LocationLink[]> {
|
||||
return getLocationLinks(model, position, ImplementationProviderRegistry, (provider, model, position) => {
|
||||
return provider.provideImplementation(model, position, token);
|
||||
});
|
||||
}
|
||||
|
||||
export function getTypeDefinitionsAtPosition(model: ITextModel, position: Position, token: CancellationToken): Promise<LocationLink[]> {
|
||||
return getLocationLinks(model, position, TypeDefinitionProviderRegistry, (provider, model, position) => {
|
||||
return provider.provideTypeDefinition(model, position, token);
|
||||
});
|
||||
}
|
||||
|
||||
export function getReferencesAtPosition(model: ITextModel, position: Position, compact: boolean, token: CancellationToken): Promise<LocationLink[]> {
|
||||
return getLocationLinks(model, position, ReferenceProviderRegistry, async (provider, model, position) => {
|
||||
const result = await provider.provideReferences(model, position, { includeDeclaration: true }, token);
|
||||
if (!compact || !result || result.length !== 2) {
|
||||
return result;
|
||||
}
|
||||
const resultWithoutDeclaration = await provider.provideReferences(model, position, { includeDeclaration: false }, token);
|
||||
if (resultWithoutDeclaration && resultWithoutDeclaration.length === 1) {
|
||||
return resultWithoutDeclaration;
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
registerModelAndPositionCommand('_executeDefinitionProvider', (model, position) => getDefinitionsAtPosition(model, position, CancellationToken.None));
|
||||
registerModelAndPositionCommand('_executeDeclarationProvider', (model, position) => getDeclarationsAtPosition(model, position, CancellationToken.None));
|
||||
registerModelAndPositionCommand('_executeImplementationProvider', (model, position) => getImplementationsAtPosition(model, position, CancellationToken.None));
|
||||
registerModelAndPositionCommand('_executeTypeDefinitionProvider', (model, position) => getTypeDefinitionsAtPosition(model, position, CancellationToken.None));
|
||||
registerModelAndPositionCommand('_executeReferenceProvider', (model, position) => getReferencesAtPosition(model, position, false, CancellationToken.None));
|
||||
@@ -0,0 +1,208 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
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';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
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 = (source.event.detail <= 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 type TriggerModifier = 'ctrlKey' | 'shiftKey' | 'altKey' | 'metaKey';
|
||||
|
||||
export class ClickLinkOptions {
|
||||
|
||||
public readonly triggerKey: KeyCode;
|
||||
public readonly triggerModifier: TriggerModifier;
|
||||
public readonly triggerSideBySideKey: KeyCode;
|
||||
public readonly triggerSideBySideModifier: TriggerModifier;
|
||||
|
||||
constructor(
|
||||
triggerKey: KeyCode,
|
||||
triggerModifier: TriggerModifier,
|
||||
triggerSideBySideKey: KeyCode,
|
||||
triggerSideBySideModifier: TriggerModifier
|
||||
) {
|
||||
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 | null]> = this._register(new Emitter<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]>());
|
||||
public readonly onMouseMoveOrRelevantKeyDown: Event<[ClickLinkMouseEvent, ClickLinkKeyboardEvent | null]> = 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 | null;
|
||||
private _hasTriggerKeyOnMouseDown: boolean;
|
||||
private _lineNumberOnMouseDown: number;
|
||||
|
||||
constructor(editor: ICodeEditor) {
|
||||
super();
|
||||
|
||||
this._editor = editor;
|
||||
this._opts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier));
|
||||
|
||||
this._lastMouseMoveEvent = null;
|
||||
this._hasTriggerKeyOnMouseDown = false;
|
||||
this._lineNumberOnMouseDown = 0;
|
||||
|
||||
this._register(this._editor.onDidChangeConfiguration((e) => {
|
||||
if (e.hasChanged(EditorOption.multiCursorModifier)) {
|
||||
const newOpts = createOptions(this._editor.getOption(EditorOption.multiCursorModifier));
|
||||
if (this._opts.equals(newOpts)) {
|
||||
return;
|
||||
}
|
||||
this._opts = newOpts;
|
||||
this._lastMouseMoveEvent = null;
|
||||
this._hasTriggerKeyOnMouseDown = false;
|
||||
this._lineNumberOnMouseDown = 0;
|
||||
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;
|
||||
this._lineNumberOnMouseDown = mouseEvent.target.position ? mouseEvent.target.position.lineNumber : 0;
|
||||
}
|
||||
|
||||
private _onEditorMouseUp(mouseEvent: ClickLinkMouseEvent): void {
|
||||
const currentLineNumber = mouseEvent.target.position ? mouseEvent.target.position.lineNumber : 0;
|
||||
if (this._hasTriggerKeyOnMouseDown && this._lineNumberOnMouseDown && this._lineNumberOnMouseDown === currentLineNumber) {
|
||||
this._onExecute.fire(mouseEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private _onEditorKeyDown(e: ClickLinkKeyboardEvent): void {
|
||||
if (
|
||||
this._lastMouseMoveEvent
|
||||
&& (
|
||||
e.keyCodeIsTriggerKey // User just pressed Ctrl/Cmd (normal goto definition)
|
||||
|| (e.keyCodeIsSideBySideKey && e.hasTriggerModifier) // User pressed Ctrl/Cmd+Alt (goto definition to the side)
|
||||
)
|
||||
) {
|
||||
this._onMouseMoveOrRelevantKeyDown.fire([this._lastMouseMoveEvent, e]);
|
||||
} else if (e.hasTriggerModifier) {
|
||||
this._onCancel.fire(); // remove decorations if user holds another key with ctrl/cmd to prevent accident goto declaration
|
||||
}
|
||||
}
|
||||
|
||||
private _onEditorKeyUp(e: ClickLinkKeyboardEvent): void {
|
||||
if (e.keyCodeIsTriggerKey) {
|
||||
this._onCancel.fire();
|
||||
}
|
||||
}
|
||||
|
||||
private _resetHandler(): void {
|
||||
this._lastMouseMoveEvent = null;
|
||||
this._hasTriggerKeyOnMouseDown = false;
|
||||
this._onCancel.fire();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .goto-definition-link {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./goToDefinitionAtPosition';
|
||||
import * as nls from 'vs/nls';
|
||||
import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IModeService } from 'vs/editor/common/services/modeService';
|
||||
import { Range, IRange } from 'vs/editor/common/core/range';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { DefinitionProviderRegistry, LocationLink } from 'vs/editor/common/modes';
|
||||
import { ICodeEditor, MouseTargetType } from 'vs/editor/browser/editorBrowser';
|
||||
import { registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { getDefinitionsAtPosition } from '../goToSymbol';
|
||||
import { DisposableStore } 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/browser/core/editorState';
|
||||
import { DefinitionAction } from '../goToCommands';
|
||||
import { ClickLinkGesture, ClickLinkMouseEvent, ClickLinkKeyboardEvent } from 'vs/editor/contrib/gotoSymbol/link/clickLinkGesture';
|
||||
import { IWordAtPosition, IModelDeltaDecoration, ITextModel, IFoundBracket } from 'vs/editor/common/model';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
import { PeekContext } from 'vs/editor/contrib/peekView/peekView';
|
||||
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class GotoDefinitionAtPositionEditorContribution implements IEditorContribution {
|
||||
|
||||
public static readonly ID = 'editor.contrib.gotodefinitionatposition';
|
||||
static readonly MAX_SOURCE_PREVIEW_LINES = 8;
|
||||
|
||||
private readonly editor: ICodeEditor;
|
||||
private readonly toUnhook = new DisposableStore();
|
||||
private readonly toUnhookForKeyboard = new DisposableStore();
|
||||
private linkDecorations: string[] = [];
|
||||
private currentWordAtPosition: IWordAtPosition | null = null;
|
||||
private previousPromise: CancelablePromise<LocationLink[] | null> | null = null;
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
@ITextModelService private readonly textModelResolverService: ITextModelService,
|
||||
@IModeService private readonly modeService: IModeService
|
||||
) {
|
||||
this.editor = editor;
|
||||
|
||||
let linkGesture = new ClickLinkGesture(editor);
|
||||
this.toUnhook.add(linkGesture);
|
||||
|
||||
this.toUnhook.add(linkGesture.onMouseMoveOrRelevantKeyDown(([mouseEvent, keyboardEvent]) => {
|
||||
this.startFindDefinitionFromMouse(mouseEvent, withNullAsUndefined(keyboardEvent));
|
||||
}));
|
||||
|
||||
this.toUnhook.add(linkGesture.onExecute((mouseEvent: ClickLinkMouseEvent) => {
|
||||
if (this.isEnabled(mouseEvent)) {
|
||||
this.gotoDefinition(mouseEvent.target.position!, mouseEvent.hasSideBySideModifier).then(() => {
|
||||
this.removeLinkDecorations();
|
||||
}, (error: Error) => {
|
||||
this.removeLinkDecorations();
|
||||
onUnexpectedError(error);
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
this.toUnhook.add(linkGesture.onCancel(() => {
|
||||
this.removeLinkDecorations();
|
||||
this.currentWordAtPosition = null;
|
||||
}));
|
||||
}
|
||||
|
||||
static get(editor: ICodeEditor): GotoDefinitionAtPositionEditorContribution {
|
||||
return editor.getContribution<GotoDefinitionAtPositionEditorContribution>(GotoDefinitionAtPositionEditorContribution.ID);
|
||||
}
|
||||
|
||||
startFindDefinitionFromCursor(position: Position) {
|
||||
// For issue: https://github.com/microsoft/vscode/issues/46257
|
||||
// equivalent to mouse move with meta/ctrl key
|
||||
|
||||
// First find the definition and add decorations
|
||||
// to the editor to be shown with the content hover widget
|
||||
return this.startFindDefinition(position).then(() => {
|
||||
|
||||
// Add listeners for editor cursor move and key down events
|
||||
// Dismiss the "extended" editor decorations when the user hides
|
||||
// the hover widget. There is no event for the widget itself so these
|
||||
// serve as a best effort. After removing the link decorations, the hover
|
||||
// widget is clean and will only show declarations per next request.
|
||||
this.toUnhookForKeyboard.add(this.editor.onDidChangeCursorPosition(() => {
|
||||
this.currentWordAtPosition = null;
|
||||
this.removeLinkDecorations();
|
||||
this.toUnhookForKeyboard.clear();
|
||||
}));
|
||||
|
||||
this.toUnhookForKeyboard.add(this.editor.onKeyDown((e: IKeyboardEvent) => {
|
||||
if (e) {
|
||||
this.currentWordAtPosition = null;
|
||||
this.removeLinkDecorations();
|
||||
this.toUnhookForKeyboard.clear();
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private startFindDefinitionFromMouse(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): void {
|
||||
|
||||
// check if we are active and on a content widget
|
||||
if (mouseEvent.target.type === MouseTargetType.CONTENT_WIDGET && this.linkDecorations.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.editor.hasModel() || !this.isEnabled(mouseEvent, withKey)) {
|
||||
this.currentWordAtPosition = null;
|
||||
this.removeLinkDecorations();
|
||||
return;
|
||||
}
|
||||
|
||||
const position = mouseEvent.target.position!;
|
||||
|
||||
this.startFindDefinition(position);
|
||||
}
|
||||
|
||||
private startFindDefinition(position: Position): Promise<number | undefined> {
|
||||
|
||||
// Dispose listeners for updating decorations when using keyboard to show definition hover
|
||||
this.toUnhookForKeyboard.clear();
|
||||
|
||||
// Find word at mouse position
|
||||
const word = position ? this.editor.getModel()?.getWordAtPosition(position) : null;
|
||||
if (!word) {
|
||||
this.currentWordAtPosition = null;
|
||||
this.removeLinkDecorations();
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
// Return early if word at position is still the same
|
||||
if (this.currentWordAtPosition && this.currentWordAtPosition.startColumn === word.startColumn && this.currentWordAtPosition.endColumn === word.endColumn && this.currentWordAtPosition.word === word.word) {
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
this.currentWordAtPosition = word;
|
||||
|
||||
// Find definition and decorate word if found
|
||||
let state = new EditorState(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection | CodeEditorStateFlag.Scroll);
|
||||
|
||||
if (this.previousPromise) {
|
||||
this.previousPromise.cancel();
|
||||
this.previousPromise = null;
|
||||
}
|
||||
|
||||
this.previousPromise = createCancelablePromise(token => this.findDefinition(position, token));
|
||||
|
||||
return this.previousPromise.then(results => {
|
||||
if (!results || !results.length || !state.validate(this.editor)) {
|
||||
this.removeLinkDecorations();
|
||||
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 (startLineNumber < 1 || startLineNumber > textEditorModel.getLineCount()) {
|
||||
// invalid range
|
||||
ref.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
const previewValue = this.getPreviewValue(textEditorModel, startLineNumber, result);
|
||||
|
||||
let wordRange: Range;
|
||||
if (result.originSelectionRange) {
|
||||
wordRange = Range.lift(result.originSelectionRange);
|
||||
} else {
|
||||
wordRange = new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn);
|
||||
}
|
||||
|
||||
const modeId = this.modeService.getModeIdByFilepathOrFirstLine(textEditorModel.uri);
|
||||
this.addDecoration(
|
||||
wordRange,
|
||||
new MarkdownString().appendCodeblock(modeId ? modeId : '', previewValue)
|
||||
);
|
||||
ref.dispose();
|
||||
});
|
||||
}
|
||||
}).then(undefined, onUnexpectedError);
|
||||
}
|
||||
|
||||
private getPreviewValue(textEditorModel: ITextModel, startLineNumber: number, result: LocationLink) {
|
||||
let rangeToUse = result.targetSelectionRange ? result.range : this.getPreviewRangeBasedOnBrackets(textEditorModel, startLineNumber);
|
||||
const numberOfLinesInRange = rangeToUse.endLineNumber - rangeToUse.startLineNumber;
|
||||
if (numberOfLinesInRange >= GotoDefinitionAtPositionEditorContribution.MAX_SOURCE_PREVIEW_LINES) {
|
||||
rangeToUse = this.getPreviewRangeBasedOnIndentation(textEditorModel, startLineNumber);
|
||||
}
|
||||
|
||||
const previewValue = this.stripIndentationFromPreviewRange(textEditorModel, startLineNumber, rangeToUse);
|
||||
return previewValue;
|
||||
}
|
||||
|
||||
private stripIndentationFromPreviewRange(textEditorModel: ITextModel, startLineNumber: number, previewRange: IRange) {
|
||||
const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber);
|
||||
let minIndent = startIndent;
|
||||
|
||||
for (let endLineNumber = startLineNumber + 1; endLineNumber < previewRange.endLineNumber; endLineNumber++) {
|
||||
const endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber);
|
||||
minIndent = Math.min(minIndent, endIndent);
|
||||
}
|
||||
|
||||
const previewValue = textEditorModel.getValueInRange(previewRange).replace(new RegExp(`^\\s{${minIndent - 1}}`, 'gm'), '').trim();
|
||||
return previewValue;
|
||||
}
|
||||
|
||||
private getPreviewRangeBasedOnIndentation(textEditorModel: ITextModel, startLineNumber: number) {
|
||||
const startIndent = textEditorModel.getLineFirstNonWhitespaceColumn(startLineNumber);
|
||||
const maxLineNumber = Math.min(textEditorModel.getLineCount(), startLineNumber + GotoDefinitionAtPositionEditorContribution.MAX_SOURCE_PREVIEW_LINES);
|
||||
let endLineNumber = startLineNumber + 1;
|
||||
|
||||
for (; endLineNumber < maxLineNumber; endLineNumber++) {
|
||||
let endIndent = textEditorModel.getLineFirstNonWhitespaceColumn(endLineNumber);
|
||||
|
||||
if (startIndent === endIndent) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return new Range(startLineNumber, 1, endLineNumber + 1, 1);
|
||||
}
|
||||
|
||||
private getPreviewRangeBasedOnBrackets(textEditorModel: ITextModel, startLineNumber: number) {
|
||||
const maxLineNumber = Math.min(textEditorModel.getLineCount(), startLineNumber + GotoDefinitionAtPositionEditorContribution.MAX_SOURCE_PREVIEW_LINES);
|
||||
|
||||
const brackets: IFoundBracket[] = [];
|
||||
|
||||
let ignoreFirstEmpty = true;
|
||||
let currentBracket = textEditorModel.findNextBracket(new Position(startLineNumber, 1));
|
||||
while (currentBracket !== null) {
|
||||
|
||||
if (brackets.length === 0) {
|
||||
brackets.push(currentBracket);
|
||||
} else {
|
||||
const lastBracket = brackets[brackets.length - 1];
|
||||
if (lastBracket.open[0] === currentBracket.open[0] && lastBracket.isOpen && !currentBracket.isOpen) {
|
||||
brackets.pop();
|
||||
} else {
|
||||
brackets.push(currentBracket);
|
||||
}
|
||||
|
||||
if (brackets.length === 0) {
|
||||
if (ignoreFirstEmpty) {
|
||||
ignoreFirstEmpty = false;
|
||||
} else {
|
||||
return new Range(startLineNumber, 1, currentBracket.range.endLineNumber + 1, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const maxColumn = textEditorModel.getLineMaxColumn(startLineNumber);
|
||||
let nextLineNumber = currentBracket.range.endLineNumber;
|
||||
let nextColumn = currentBracket.range.endColumn;
|
||||
if (maxColumn === currentBracket.range.endColumn) {
|
||||
nextLineNumber++;
|
||||
nextColumn = 1;
|
||||
}
|
||||
|
||||
if (nextLineNumber > maxLineNumber) {
|
||||
return new Range(startLineNumber, 1, maxLineNumber + 1, 1);
|
||||
}
|
||||
|
||||
currentBracket = textEditorModel.findNextBracket(new Position(nextLineNumber, nextColumn));
|
||||
}
|
||||
|
||||
return new Range(startLineNumber, 1, maxLineNumber + 1, 1);
|
||||
}
|
||||
|
||||
private addDecoration(range: Range, hoverMessage: MarkdownString): void {
|
||||
|
||||
const newDecorations: IModelDeltaDecoration = {
|
||||
range: range,
|
||||
options: {
|
||||
inlineClassName: 'goto-definition-link',
|
||||
hoverMessage
|
||||
}
|
||||
};
|
||||
|
||||
this.linkDecorations = this.editor.deltaDecorations(this.linkDecorations, [newDecorations]);
|
||||
}
|
||||
|
||||
private removeLinkDecorations(): void {
|
||||
if (this.linkDecorations.length > 0) {
|
||||
this.linkDecorations = this.editor.deltaDecorations(this.linkDecorations, []);
|
||||
}
|
||||
}
|
||||
|
||||
private isEnabled(mouseEvent: ClickLinkMouseEvent, withKey?: ClickLinkKeyboardEvent): boolean {
|
||||
return this.editor.hasModel() &&
|
||||
mouseEvent.isNoneOrSingleMouseDown &&
|
||||
(mouseEvent.target.type === MouseTargetType.CONTENT_TEXT) &&
|
||||
(mouseEvent.hasTriggerModifier || (withKey ? withKey.keyCodeIsTriggerKey : false)) &&
|
||||
DefinitionProviderRegistry.has(this.editor.getModel());
|
||||
}
|
||||
|
||||
private findDefinition(position: Position, token: CancellationToken): Promise<LocationLink[] | null> {
|
||||
const model = this.editor.getModel();
|
||||
if (!model) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return getDefinitionsAtPosition(model, position, token);
|
||||
}
|
||||
|
||||
private gotoDefinition(position: Position, openToSide: boolean): Promise<any> {
|
||||
this.editor.setPosition(position);
|
||||
return this.editor.invokeWithinContext((accessor) => {
|
||||
const canPeek = !openToSide && this.editor.getOption(EditorOption.definitionLinkOpensInPeek) && !this.isInPeekEditor(accessor);
|
||||
const action = new DefinitionAction({ openToSide, openInPeek: canPeek, muteMessage: true }, { alias: '', label: '', id: '', precondition: undefined });
|
||||
return action.run(accessor, this.editor);
|
||||
});
|
||||
}
|
||||
|
||||
private isInPeekEditor(accessor: ServicesAccessor): boolean | undefined {
|
||||
const contextKeyService = accessor.get(IContextKeyService);
|
||||
return PeekContext.inPeekEditor.getValue(contextKeyService);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.toUnhook.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
registerEditorContribution(GotoDefinitionAtPositionEditorContribution.ID, GotoDefinitionAtPositionEditorContribution);
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const activeLinkForeground = theme.getColor(editorActiveLinkForeground);
|
||||
if (activeLinkForeground) {
|
||||
collector.addRule(`.monaco-editor .goto-definition-link { color: ${activeLinkForeground} !important; }`);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,418 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IContextKey, IContextKeyService, RawContextKey, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ReferencesModel, OneReference } from '../referencesModel';
|
||||
import { ReferenceWidget, LayoutData } from './referencesWidget';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { Location } from 'vs/editor/common/modes';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { getOuterEditor, PeekContext } from 'vs/editor/contrib/peekView/peekView';
|
||||
import { IListService, WorkbenchListFocusContextKey } from 'vs/platform/list/browser/listService';
|
||||
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes';
|
||||
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
|
||||
import { EditorOption } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
export const ctxReferenceSearchVisible = new RawContextKey<boolean>('referenceSearchVisible', false);
|
||||
|
||||
export abstract class ReferencesController implements IEditorContribution {
|
||||
|
||||
static readonly ID = 'editor.contrib.referencesController';
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
private _widget?: ReferenceWidget;
|
||||
private _model?: ReferencesModel;
|
||||
private _peekMode?: boolean;
|
||||
private _requestIdPool = 0;
|
||||
private _ignoreModelChangeEvent = false;
|
||||
|
||||
private readonly _referenceSearchVisible: IContextKey<boolean>;
|
||||
|
||||
static get(editor: ICodeEditor): ReferencesController {
|
||||
return editor.getContribution<ReferencesController>(ReferencesController.ID);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly _defaultTreeKeyboardSupport: boolean,
|
||||
private readonly _editor: ICodeEditor,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
) {
|
||||
|
||||
this._referenceSearchVisible = ctxReferenceSearchVisible.bindTo(contextKeyService);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._referenceSearchVisible.reset();
|
||||
this._disposables.dispose();
|
||||
this._widget?.dispose();
|
||||
this._model?.dispose();
|
||||
this._widget = undefined;
|
||||
this._model = undefined;
|
||||
}
|
||||
|
||||
toggleWidget(range: Range, modelPromise: CancelablePromise<ReferencesModel>, peekMode: boolean): void {
|
||||
|
||||
// close current widget and return early is position didn't change
|
||||
let widgetPosition: Position | undefined;
|
||||
if (this._widget) {
|
||||
widgetPosition = this._widget.position;
|
||||
}
|
||||
this.closeWidget();
|
||||
if (!!widgetPosition && range.containsPosition(widgetPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._peekMode = peekMode;
|
||||
this._referenceSearchVisible.set(true);
|
||||
|
||||
// close the widget on model/mode changes
|
||||
this._disposables.add(this._editor.onDidChangeModelLanguage(() => { this.closeWidget(); }));
|
||||
this._disposables.add(this._editor.onDidChangeModel(() => {
|
||||
if (!this._ignoreModelChangeEvent) {
|
||||
this.closeWidget();
|
||||
}
|
||||
}));
|
||||
const storageKey = 'peekViewLayout';
|
||||
const data = LayoutData.fromJSON(this._storageService.get(storageKey, StorageScope.GLOBAL, '{}'));
|
||||
this._widget = this._instantiationService.createInstance(ReferenceWidget, this._editor, this._defaultTreeKeyboardSupport, data);
|
||||
this._widget.setTitle(nls.localize('labelLoading', "Loading..."));
|
||||
this._widget.show(range);
|
||||
|
||||
this._disposables.add(this._widget.onDidClose(() => {
|
||||
modelPromise.cancel();
|
||||
if (this._widget) {
|
||||
this._storageService.store(storageKey, JSON.stringify(this._widget.layoutData), StorageScope.GLOBAL);
|
||||
this._widget = undefined;
|
||||
}
|
||||
this.closeWidget();
|
||||
}));
|
||||
|
||||
this._disposables.add(this._widget.onDidSelectReference(event => {
|
||||
let { element, kind } = event;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
switch (kind) {
|
||||
case 'open':
|
||||
if (event.source !== 'editor' || !this._configurationService.getValue('editor.stablePeek')) {
|
||||
// when stable peek is configured we don't close
|
||||
// the peek window on selecting the editor
|
||||
this.openReference(element, false);
|
||||
}
|
||||
break;
|
||||
case 'side':
|
||||
this.openReference(element, true);
|
||||
break;
|
||||
case 'goto':
|
||||
if (peekMode) {
|
||||
this._gotoReference(element);
|
||||
} else {
|
||||
this.openReference(element, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
const requestId = ++this._requestIdPool;
|
||||
|
||||
modelPromise.then(model => {
|
||||
|
||||
// still current request? widget still open?
|
||||
if (requestId !== this._requestIdPool || !this._widget) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this._model) {
|
||||
this._model.dispose();
|
||||
}
|
||||
|
||||
this._model = model;
|
||||
|
||||
// show widget
|
||||
return this._widget.setModel(this._model).then(() => {
|
||||
if (this._widget && this._model && this._editor.hasModel()) { // might have been closed
|
||||
|
||||
// set title
|
||||
if (!this._model.isEmpty) {
|
||||
this._widget.setMetaTitle(nls.localize('metaTitle.N', "{0} ({1})", this._model.title, this._model.references.length));
|
||||
} else {
|
||||
this._widget.setMetaTitle('');
|
||||
}
|
||||
|
||||
// set 'best' selection
|
||||
let uri = this._editor.getModel().uri;
|
||||
let pos = new Position(range.startLineNumber, range.startColumn);
|
||||
let selection = this._model.nearestReference(uri, pos);
|
||||
if (selection) {
|
||||
return this._widget.setSelection(selection).then(() => {
|
||||
if (this._widget && this._editor.getOption(EditorOption.peekWidgetDefaultFocus) === 'editor') {
|
||||
this._widget.focusOnPreviewEditor();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
}, error => {
|
||||
this._notificationService.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
changeFocusBetweenPreviewAndReferences() {
|
||||
if (!this._widget) {
|
||||
// can be called while still resolving...
|
||||
return;
|
||||
}
|
||||
if (this._widget.isPreviewEditorFocused()) {
|
||||
this._widget.focusOnReferenceTree();
|
||||
} else {
|
||||
this._widget.focusOnPreviewEditor();
|
||||
}
|
||||
}
|
||||
|
||||
async goToNextOrPreviousReference(fwd: boolean) {
|
||||
if (!this._editor.hasModel() || !this._model || !this._widget) {
|
||||
// can be called while still resolving...
|
||||
return;
|
||||
}
|
||||
const currentPosition = this._widget.position;
|
||||
if (!currentPosition) {
|
||||
return;
|
||||
}
|
||||
const source = this._model.nearestReference(this._editor.getModel().uri, currentPosition);
|
||||
if (!source) {
|
||||
return;
|
||||
}
|
||||
const target = this._model.nextOrPreviousReference(source, fwd);
|
||||
const editorFocus = this._editor.hasTextFocus();
|
||||
const previewEditorFocus = this._widget.isPreviewEditorFocused();
|
||||
await this._widget.setSelection(target);
|
||||
await this._gotoReference(target);
|
||||
if (editorFocus) {
|
||||
this._editor.focus();
|
||||
} else if (this._widget && previewEditorFocus) {
|
||||
this._widget.focusOnPreviewEditor();
|
||||
}
|
||||
}
|
||||
|
||||
async revealReference(reference: OneReference): Promise<void> {
|
||||
if (!this._editor.hasModel() || !this._model || !this._widget) {
|
||||
// can be called while still resolving...
|
||||
return;
|
||||
}
|
||||
|
||||
await this._widget.revealReference(reference);
|
||||
}
|
||||
|
||||
closeWidget(focusEditor = true): void {
|
||||
this._widget?.dispose();
|
||||
this._model?.dispose();
|
||||
this._referenceSearchVisible.reset();
|
||||
this._disposables.clear();
|
||||
this._widget = undefined;
|
||||
this._model = undefined;
|
||||
if (focusEditor) {
|
||||
this._editor.focus();
|
||||
}
|
||||
this._requestIdPool += 1; // Cancel pending requests
|
||||
}
|
||||
|
||||
private _gotoReference(ref: Location): Promise<any> {
|
||||
if (this._widget) {
|
||||
this._widget.hide();
|
||||
}
|
||||
|
||||
this._ignoreModelChangeEvent = true;
|
||||
const range = Range.lift(ref.range).collapseToStart();
|
||||
|
||||
return this._editorService.openCodeEditor({
|
||||
resource: ref.uri,
|
||||
options: { selection: range }
|
||||
}, this._editor).then(openedEditor => {
|
||||
this._ignoreModelChangeEvent = false;
|
||||
|
||||
if (!openedEditor || !this._widget) {
|
||||
// something went wrong...
|
||||
this.closeWidget();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._editor === openedEditor) {
|
||||
//
|
||||
this._widget.show(range);
|
||||
this._widget.focusOnReferenceTree();
|
||||
|
||||
} else {
|
||||
// we opened a different editor instance which means a different controller instance.
|
||||
// therefore we stop with this controller and continue with the other
|
||||
const other = ReferencesController.get(openedEditor);
|
||||
const model = this._model!.clone();
|
||||
|
||||
this.closeWidget();
|
||||
openedEditor.focus();
|
||||
|
||||
other.toggleWidget(
|
||||
range,
|
||||
createCancelablePromise(_ => Promise.resolve(model)),
|
||||
this._peekMode ?? false
|
||||
);
|
||||
}
|
||||
|
||||
}, (err) => {
|
||||
this._ignoreModelChangeEvent = false;
|
||||
onUnexpectedError(err);
|
||||
});
|
||||
}
|
||||
|
||||
openReference(ref: Location, sideBySide: boolean): void {
|
||||
// clear stage
|
||||
if (!sideBySide) {
|
||||
this.closeWidget();
|
||||
}
|
||||
|
||||
const { uri, range } = ref;
|
||||
this._editorService.openCodeEditor({
|
||||
resource: uri,
|
||||
options: { selection: range }
|
||||
}, this._editor, sideBySide);
|
||||
}
|
||||
}
|
||||
|
||||
function withController(accessor: ServicesAccessor, fn: (controller: ReferencesController) => void): void {
|
||||
const outerEditor = getOuterEditor(accessor);
|
||||
if (!outerEditor) {
|
||||
return;
|
||||
}
|
||||
let controller = ReferencesController.get(outerEditor);
|
||||
if (controller) {
|
||||
fn(controller);
|
||||
}
|
||||
}
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'togglePeekWidgetFocus',
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyCode.F2),
|
||||
when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor),
|
||||
handler(accessor) {
|
||||
withController(accessor, controller => {
|
||||
controller.changeFocusBetweenPreviewAndReferences();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'goToNextReference',
|
||||
weight: KeybindingWeight.EditorContrib - 10,
|
||||
primary: KeyCode.F4,
|
||||
secondary: [KeyCode.F12],
|
||||
when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor),
|
||||
handler(accessor) {
|
||||
withController(accessor, controller => {
|
||||
controller.goToNextOrPreviousReference(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'goToPreviousReference',
|
||||
weight: KeybindingWeight.EditorContrib - 10,
|
||||
primary: KeyMod.Shift | KeyCode.F4,
|
||||
secondary: [KeyMod.Shift | KeyCode.F12],
|
||||
when: ContextKeyExpr.or(ctxReferenceSearchVisible, PeekContext.inPeekEditor),
|
||||
handler(accessor) {
|
||||
withController(accessor, controller => {
|
||||
controller.goToNextOrPreviousReference(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// commands that aren't needed anymore because there is now ContextKeyExpr.OR
|
||||
CommandsRegistry.registerCommandAlias('goToNextReferenceFromEmbeddedEditor', 'goToNextReference');
|
||||
CommandsRegistry.registerCommandAlias('goToPreviousReferenceFromEmbeddedEditor', 'goToPreviousReference');
|
||||
|
||||
// close
|
||||
CommandsRegistry.registerCommandAlias('closeReferenceSearchEditor', 'closeReferenceSearch');
|
||||
CommandsRegistry.registerCommand(
|
||||
'closeReferenceSearch',
|
||||
accessor => withController(accessor, controller => controller.closeWidget())
|
||||
);
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
id: 'closeReferenceSearch',
|
||||
weight: KeybindingWeight.EditorContrib - 101,
|
||||
primary: KeyCode.Escape,
|
||||
secondary: [KeyMod.Shift | KeyCode.Escape],
|
||||
when: ContextKeyExpr.and(PeekContext.inPeekEditor, ContextKeyExpr.not('config.editor.stablePeek'))
|
||||
});
|
||||
KeybindingsRegistry.registerKeybindingRule({
|
||||
id: 'closeReferenceSearch',
|
||||
weight: KeybindingWeight.WorkbenchContrib + 50,
|
||||
primary: KeyCode.Escape,
|
||||
secondary: [KeyMod.Shift | KeyCode.Escape],
|
||||
when: ContextKeyExpr.and(ctxReferenceSearchVisible, ContextKeyExpr.not('config.editor.stablePeek'))
|
||||
});
|
||||
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'revealReference',
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyCode.Enter,
|
||||
mac: {
|
||||
primary: KeyCode.Enter,
|
||||
secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow]
|
||||
},
|
||||
when: ContextKeyExpr.and(ctxReferenceSearchVisible, WorkbenchListFocusContextKey),
|
||||
handler(accessor: ServicesAccessor) {
|
||||
const listService = accessor.get(IListService);
|
||||
const focus = <any[]>listService.lastFocusedList?.getFocus();
|
||||
if (Array.isArray(focus) && focus[0] instanceof OneReference) {
|
||||
withController(accessor, controller => controller.revealReference(focus[0]));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'openReferenceToSide',
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.Enter,
|
||||
mac: {
|
||||
primary: KeyMod.WinCtrl | KeyCode.Enter
|
||||
},
|
||||
when: ContextKeyExpr.and(ctxReferenceSearchVisible, WorkbenchListFocusContextKey),
|
||||
handler(accessor: ServicesAccessor) {
|
||||
const listService = accessor.get(IListService);
|
||||
const focus = <any[]>listService.lastFocusedList?.getFocus();
|
||||
if (Array.isArray(focus) && focus[0] instanceof OneReference) {
|
||||
withController(accessor, controller => controller.openReference(focus[0], true));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
CommandsRegistry.registerCommand('openReference', (accessor) => {
|
||||
const listService = accessor.get(IListService);
|
||||
const focus = <any[]>listService.lastFocusedList?.getFocus();
|
||||
if (Array.isArray(focus) && focus[0] instanceof OneReference) {
|
||||
withController(accessor, controller => controller.openReference(focus[0], false));
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,221 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ReferencesModel, FileReferences, OneReference } from '../referencesModel';
|
||||
import { ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { ITreeRenderer, ITreeNode, IAsyncDataSource } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IconLabel } from 'vs/base/browser/ui/iconLabel/iconLabel';
|
||||
import { CountBadge } from 'vs/base/browser/ui/countBadge/countBadge';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { attachBadgeStyler } from 'vs/platform/theme/common/styler';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { localize } from 'vs/nls';
|
||||
import { getBaseLabel } from 'vs/base/common/labels';
|
||||
import { dirname, basename } from 'vs/base/common/resources';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IListAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IListVirtualDelegate, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { FuzzyScore, createMatches, IMatch } from 'vs/base/common/filters';
|
||||
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
|
||||
//#region data source
|
||||
|
||||
export type TreeElement = FileReferences | OneReference;
|
||||
|
||||
export class DataSource implements IAsyncDataSource<ReferencesModel | FileReferences, TreeElement> {
|
||||
|
||||
constructor(@ITextModelService private readonly _resolverService: ITextModelService) { }
|
||||
|
||||
hasChildren(element: ReferencesModel | FileReferences | TreeElement): boolean {
|
||||
if (element instanceof ReferencesModel) {
|
||||
return true;
|
||||
}
|
||||
if (element instanceof FileReferences) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getChildren(element: ReferencesModel | FileReferences | TreeElement): TreeElement[] | Promise<TreeElement[]> {
|
||||
if (element instanceof ReferencesModel) {
|
||||
return element.groups;
|
||||
}
|
||||
|
||||
if (element instanceof FileReferences) {
|
||||
return element.resolve(this._resolverService).then(val => {
|
||||
// if (element.failure) {
|
||||
// // refresh the element on failure so that
|
||||
// // we can update its rendering
|
||||
// return tree.refresh(element).then(() => val.children);
|
||||
// }
|
||||
return val.children;
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error('bad tree');
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
export class Delegate implements IListVirtualDelegate<TreeElement> {
|
||||
getHeight(): number {
|
||||
return 23;
|
||||
}
|
||||
getTemplateId(element: FileReferences | OneReference): string {
|
||||
if (element instanceof FileReferences) {
|
||||
return FileReferencesRenderer.id;
|
||||
} else {
|
||||
return OneReferenceRenderer.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class StringRepresentationProvider implements IKeyboardNavigationLabelProvider<TreeElement> {
|
||||
|
||||
constructor(@IKeybindingService private readonly _keybindingService: IKeybindingService) { }
|
||||
|
||||
getKeyboardNavigationLabel(element: TreeElement): { toString(): string; } {
|
||||
if (element instanceof OneReference) {
|
||||
const parts = element.parent.getPreview(element)?.preview(element.range);
|
||||
if (parts) {
|
||||
return parts.value;
|
||||
}
|
||||
}
|
||||
// FileReferences or unresolved OneReference
|
||||
return basename(element.uri);
|
||||
}
|
||||
|
||||
mightProducePrintableCharacter(event: IKeyboardEvent): boolean {
|
||||
return this._keybindingService.mightProducePrintableCharacter(event);
|
||||
}
|
||||
}
|
||||
|
||||
export class IdentityProvider implements IIdentityProvider<TreeElement> {
|
||||
|
||||
getId(element: TreeElement): { toString(): string; } {
|
||||
return element instanceof OneReference ? element.id : element.uri;
|
||||
}
|
||||
}
|
||||
|
||||
//#region render: File
|
||||
|
||||
class FileReferencesTemplate extends Disposable {
|
||||
|
||||
readonly file: IconLabel;
|
||||
readonly badge: CountBadge;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@ILabelService private readonly _uriLabel: ILabelService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
) {
|
||||
super();
|
||||
const parent = document.createElement('div');
|
||||
parent.classList.add('reference-file');
|
||||
this.file = this._register(new IconLabel(parent, { supportHighlights: true }));
|
||||
|
||||
this.badge = new CountBadge(dom.append(parent, dom.$('.count')));
|
||||
this._register(attachBadgeStyler(this.badge, themeService));
|
||||
|
||||
container.appendChild(parent);
|
||||
}
|
||||
|
||||
set(element: FileReferences, matches: IMatch[]) {
|
||||
let parent = dirname(element.uri);
|
||||
this.file.setLabel(getBaseLabel(element.uri), this._uriLabel.getUriLabel(parent, { relative: true }), { title: this._uriLabel.getUriLabel(element.uri), matches });
|
||||
const len = element.children.length;
|
||||
this.badge.setCount(len);
|
||||
if (len > 1) {
|
||||
this.badge.setTitleFormat(localize('referencesCount', "{0} references", len));
|
||||
} else {
|
||||
this.badge.setTitleFormat(localize('referenceCount', "{0} reference", len));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class FileReferencesRenderer implements ITreeRenderer<FileReferences, FuzzyScore, FileReferencesTemplate> {
|
||||
|
||||
static readonly id = 'FileReferencesRenderer';
|
||||
|
||||
readonly templateId: string = FileReferencesRenderer.id;
|
||||
|
||||
constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { }
|
||||
|
||||
renderTemplate(container: HTMLElement): FileReferencesTemplate {
|
||||
return this._instantiationService.createInstance(FileReferencesTemplate, container);
|
||||
}
|
||||
renderElement(node: ITreeNode<FileReferences, FuzzyScore>, index: number, template: FileReferencesTemplate): void {
|
||||
template.set(node.element, createMatches(node.filterData));
|
||||
}
|
||||
disposeTemplate(templateData: FileReferencesTemplate): void {
|
||||
templateData.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region render: Reference
|
||||
class OneReferenceTemplate {
|
||||
|
||||
readonly label: HighlightedLabel;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.label = new HighlightedLabel(container, false);
|
||||
}
|
||||
|
||||
set(element: OneReference, score?: FuzzyScore): void {
|
||||
const preview = element.parent.getPreview(element)?.preview(element.range);
|
||||
if (!preview || !preview.value) {
|
||||
// this means we FAILED to resolve the document or the value is the empty string
|
||||
this.label.set(`${basename(element.uri)}:${element.range.startLineNumber + 1}:${element.range.startColumn + 1}`);
|
||||
} else {
|
||||
// render search match as highlight unless
|
||||
// we have score, then render the score
|
||||
const { value, highlight } = preview;
|
||||
if (score && !FuzzyScore.isDefault(score)) {
|
||||
this.label.element.classList.toggle('referenceMatch', false);
|
||||
this.label.set(value, createMatches(score));
|
||||
} else {
|
||||
this.label.element.classList.toggle('referenceMatch', true);
|
||||
this.label.set(value, [highlight]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OneReferenceRenderer implements ITreeRenderer<OneReference, FuzzyScore, OneReferenceTemplate> {
|
||||
|
||||
static readonly id = 'OneReferenceRenderer';
|
||||
|
||||
readonly templateId: string = OneReferenceRenderer.id;
|
||||
|
||||
renderTemplate(container: HTMLElement): OneReferenceTemplate {
|
||||
return new OneReferenceTemplate(container);
|
||||
}
|
||||
renderElement(node: ITreeNode<OneReference, FuzzyScore>, index: number, templateData: OneReferenceTemplate): void {
|
||||
templateData.set(node.element, node.filterData);
|
||||
}
|
||||
disposeTemplate(): void {
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
export class AccessibilityProvider implements IListAccessibilityProvider<FileReferences | OneReference> {
|
||||
|
||||
getWidgetAriaLabel(): string {
|
||||
return localize('treeAriaLabel', "References");
|
||||
}
|
||||
|
||||
getAriaLabel(element: FileReferences | OneReference): string | null {
|
||||
return element.ariaMessage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* -- zone widget */
|
||||
.monaco-editor .zone-widget .zone-widget-container.reference-zone-widget {
|
||||
border-top-width: 1px;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
|
||||
.monaco-editor .reference-zone-widget .inline {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.monaco-editor .reference-zone-widget .messages {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 3em 0;
|
||||
}
|
||||
|
||||
.monaco-editor .reference-zone-widget .ref-tree {
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
.monaco-editor .reference-zone-widget .ref-tree .reference {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-editor .reference-zone-widget .ref-tree .reference-file {
|
||||
display: inline-flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-editor .reference-zone-widget .ref-tree .monaco-list:focus .selected .reference-file {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.monaco-editor .reference-zone-widget .ref-tree .reference-file .count {
|
||||
margin-right: 12px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* High Contrast Theming */
|
||||
|
||||
.monaco-editor.hc-black .reference-zone-widget .ref-tree .reference-file {
|
||||
font-weight: bold;
|
||||
}
|
||||
@@ -0,0 +1,611 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./referencesWidget';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { dispose, IDisposable, IReference, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { basenameOrAuthority, dirname } from 'vs/base/common/resources';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EmbeddedCodeEditorWidget } from 'vs/editor/browser/widget/embeddedCodeEditorWidget';
|
||||
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { IModelDeltaDecoration, TrackedRangeStickiness } from 'vs/editor/common/model';
|
||||
import { ModelDecorationOptions, TextModel } from 'vs/editor/common/model/textModel';
|
||||
import { Location } from 'vs/editor/common/modes';
|
||||
import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService';
|
||||
import { AccessibilityProvider, DataSource, Delegate, FileReferencesRenderer, OneReferenceRenderer, TreeElement, StringRepresentationProvider, IdentityProvider } from 'vs/editor/contrib/gotoSymbol/peek/referencesTree';
|
||||
import * as nls from 'vs/nls';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILabelService } from 'vs/platform/label/common/label';
|
||||
import { WorkbenchAsyncDataTree, IWorkbenchAsyncDataTreeOptions } from 'vs/platform/list/browser/listService';
|
||||
import { activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
|
||||
import { IColorTheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
|
||||
import * as peekView from 'vs/editor/contrib/peekView/peekView';
|
||||
import { FileReferences, OneReference, ReferencesModel } from '../referencesModel';
|
||||
import { FuzzyScore } from 'vs/base/common/filters';
|
||||
import { SplitView, Sizing } from 'vs/base/browser/ui/splitview/splitview';
|
||||
import { IUndoRedoService } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
|
||||
|
||||
class DecorationsManager implements IDisposable {
|
||||
|
||||
private static readonly DecorationOptions = ModelDecorationOptions.register({
|
||||
stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
|
||||
className: 'reference-decoration'
|
||||
});
|
||||
|
||||
private _decorations = new Map<string, OneReference>();
|
||||
private _decorationIgnoreSet = new Set<string>();
|
||||
private readonly _callOnDispose = new DisposableStore();
|
||||
private readonly _callOnModelChange = new DisposableStore();
|
||||
|
||||
constructor(private _editor: ICodeEditor, private _model: ReferencesModel) {
|
||||
this._callOnDispose.add(this._editor.onDidChangeModel(() => this._onModelChanged()));
|
||||
this._onModelChanged();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._callOnModelChange.dispose();
|
||||
this._callOnDispose.dispose();
|
||||
this.removeDecorations();
|
||||
}
|
||||
|
||||
private _onModelChanged(): void {
|
||||
this._callOnModelChange.clear();
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
for (let ref of this._model.references) {
|
||||
if (ref.uri.toString() === model.uri.toString()) {
|
||||
this._addDecorations(ref.parent);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _addDecorations(reference: FileReferences): void {
|
||||
if (!this._editor.hasModel()) {
|
||||
return;
|
||||
}
|
||||
this._callOnModelChange.add(this._editor.getModel().onDidChangeDecorations(() => this._onDecorationChanged()));
|
||||
|
||||
const newDecorations: IModelDeltaDecoration[] = [];
|
||||
const newDecorationsActualIndex: number[] = [];
|
||||
|
||||
for (let i = 0, len = reference.children.length; i < len; i++) {
|
||||
let oneReference = reference.children[i];
|
||||
if (this._decorationIgnoreSet.has(oneReference.id)) {
|
||||
continue;
|
||||
}
|
||||
if (oneReference.uri.toString() !== this._editor.getModel().uri.toString()) {
|
||||
continue;
|
||||
}
|
||||
newDecorations.push({
|
||||
range: oneReference.range,
|
||||
options: DecorationsManager.DecorationOptions
|
||||
});
|
||||
newDecorationsActualIndex.push(i);
|
||||
}
|
||||
|
||||
const decorations = this._editor.deltaDecorations([], newDecorations);
|
||||
for (let i = 0; i < decorations.length; i++) {
|
||||
this._decorations.set(decorations[i], reference.children[newDecorationsActualIndex[i]]);
|
||||
}
|
||||
}
|
||||
|
||||
private _onDecorationChanged(): void {
|
||||
const toRemove: string[] = [];
|
||||
|
||||
const model = this._editor.getModel();
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let [decorationId, reference] of this._decorations) {
|
||||
|
||||
const newRange = model.getDecorationRange(decorationId);
|
||||
|
||||
if (!newRange) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let ignore = false;
|
||||
if (Range.equalsRange(newRange, reference.range)) {
|
||||
continue;
|
||||
|
||||
}
|
||||
|
||||
if (Range.spansMultipleLines(newRange)) {
|
||||
ignore = true;
|
||||
|
||||
} else {
|
||||
const lineLength = reference.range.endColumn - reference.range.startColumn;
|
||||
const newLineLength = newRange.endColumn - newRange.startColumn;
|
||||
|
||||
if (lineLength !== newLineLength) {
|
||||
ignore = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ignore) {
|
||||
this._decorationIgnoreSet.add(reference.id);
|
||||
toRemove.push(decorationId);
|
||||
} else {
|
||||
reference.range = newRange;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0, len = toRemove.length; i < len; i++) {
|
||||
this._decorations.delete(toRemove[i]);
|
||||
}
|
||||
this._editor.deltaDecorations(toRemove, []);
|
||||
}
|
||||
|
||||
removeDecorations(): void {
|
||||
this._editor.deltaDecorations([...this._decorations.keys()], []);
|
||||
this._decorations.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export class LayoutData {
|
||||
ratio: number = 0.7;
|
||||
heightInLines: number = 18;
|
||||
|
||||
static fromJSON(raw: string): LayoutData {
|
||||
let ratio: number | undefined;
|
||||
let heightInLines: number | undefined;
|
||||
try {
|
||||
const data = <LayoutData>JSON.parse(raw);
|
||||
ratio = data.ratio;
|
||||
heightInLines = data.heightInLines;
|
||||
} catch {
|
||||
//
|
||||
}
|
||||
return {
|
||||
ratio: ratio || 0.7,
|
||||
heightInLines: heightInLines || 18
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface SelectionEvent {
|
||||
readonly kind: 'goto' | 'show' | 'side' | 'open';
|
||||
readonly source: 'editor' | 'tree' | 'title';
|
||||
readonly element?: Location;
|
||||
}
|
||||
|
||||
class ReferencesTree extends WorkbenchAsyncDataTree<ReferencesModel | FileReferences, TreeElement, FuzzyScore> { }
|
||||
|
||||
/**
|
||||
* ZoneWidget that is shown inside the editor
|
||||
*/
|
||||
export class ReferenceWidget extends peekView.PeekViewWidget {
|
||||
|
||||
private _model?: ReferencesModel;
|
||||
private _decorationsManager?: DecorationsManager;
|
||||
|
||||
private readonly _disposeOnNewModel = new DisposableStore();
|
||||
private readonly _callOnDispose = new DisposableStore();
|
||||
|
||||
private readonly _onDidSelectReference = new Emitter<SelectionEvent>();
|
||||
readonly onDidSelectReference = this._onDidSelectReference.event;
|
||||
|
||||
private _tree!: ReferencesTree;
|
||||
private _treeContainer!: HTMLElement;
|
||||
private _splitView!: SplitView;
|
||||
private _preview!: ICodeEditor;
|
||||
private _previewModelReference!: IReference<ITextEditorModel>;
|
||||
private _previewNotAvailableMessage!: TextModel;
|
||||
private _previewContainer!: HTMLElement;
|
||||
private _messageContainer!: HTMLElement;
|
||||
private _dim = new dom.Dimension(0, 0);
|
||||
|
||||
constructor(
|
||||
editor: ICodeEditor,
|
||||
private _defaultTreeKeyboardSupport: boolean,
|
||||
public layoutData: LayoutData,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@ITextModelService private readonly _textModelResolverService: ITextModelService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@peekView.IPeekViewService private readonly _peekViewService: peekView.IPeekViewService,
|
||||
@ILabelService private readonly _uriLabel: ILabelService,
|
||||
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
) {
|
||||
super(editor, { showFrame: false, showArrow: true, isResizeable: true, isAccessible: true }, _instantiationService);
|
||||
|
||||
this._applyTheme(themeService.getColorTheme());
|
||||
this._callOnDispose.add(themeService.onDidColorThemeChange(this._applyTheme.bind(this)));
|
||||
this._peekViewService.addExclusiveWidget(editor, this);
|
||||
this.create();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.setModel(undefined);
|
||||
this._callOnDispose.dispose();
|
||||
this._disposeOnNewModel.dispose();
|
||||
dispose(this._preview);
|
||||
dispose(this._previewNotAvailableMessage);
|
||||
dispose(this._tree);
|
||||
dispose(this._previewModelReference);
|
||||
this._splitView.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _applyTheme(theme: IColorTheme) {
|
||||
const borderColor = theme.getColor(peekView.peekViewBorder) || Color.transparent;
|
||||
this.style({
|
||||
arrowColor: borderColor,
|
||||
frameColor: borderColor,
|
||||
headerBackgroundColor: theme.getColor(peekView.peekViewTitleBackground) || Color.transparent,
|
||||
primaryHeadingColor: theme.getColor(peekView.peekViewTitleForeground),
|
||||
secondaryHeadingColor: theme.getColor(peekView.peekViewTitleInfoForeground)
|
||||
});
|
||||
}
|
||||
|
||||
show(where: IRange) {
|
||||
this.editor.revealRangeInCenterIfOutsideViewport(where, ScrollType.Smooth);
|
||||
super.show(where, this.layoutData.heightInLines || 18);
|
||||
}
|
||||
|
||||
focusOnReferenceTree(): void {
|
||||
this._tree.domFocus();
|
||||
}
|
||||
|
||||
focusOnPreviewEditor(): void {
|
||||
this._preview.focus();
|
||||
}
|
||||
|
||||
isPreviewEditorFocused(): boolean {
|
||||
return this._preview.hasTextFocus();
|
||||
}
|
||||
|
||||
protected _onTitleClick(e: IMouseEvent): void {
|
||||
if (this._preview && this._preview.getModel()) {
|
||||
this._onDidSelectReference.fire({
|
||||
element: this._getFocusedReference(),
|
||||
kind: e.ctrlKey || e.metaKey || e.altKey ? 'side' : 'open',
|
||||
source: 'title'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected _fillBody(containerElement: HTMLElement): void {
|
||||
this.setCssClass('reference-zone-widget');
|
||||
|
||||
// message pane
|
||||
this._messageContainer = dom.append(containerElement, dom.$('div.messages'));
|
||||
dom.hide(this._messageContainer);
|
||||
|
||||
this._splitView = new SplitView(containerElement, { orientation: Orientation.HORIZONTAL });
|
||||
|
||||
// editor
|
||||
this._previewContainer = dom.append(containerElement, dom.$('div.preview.inline'));
|
||||
let options: IEditorOptions = {
|
||||
scrollBeyondLastLine: false,
|
||||
scrollbar: {
|
||||
verticalScrollbarSize: 14,
|
||||
horizontal: 'auto',
|
||||
useShadows: true,
|
||||
verticalHasArrows: false,
|
||||
horizontalHasArrows: false,
|
||||
alwaysConsumeMouseWheel: false
|
||||
},
|
||||
overviewRulerLanes: 2,
|
||||
fixedOverflowWidgets: true,
|
||||
minimap: {
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
this._preview = this._instantiationService.createInstance(EmbeddedCodeEditorWidget, this._previewContainer, options, this.editor);
|
||||
dom.hide(this._previewContainer);
|
||||
this._previewNotAvailableMessage = new TextModel(nls.localize('missingPreviewMessage', "no preview available"), TextModel.DEFAULT_CREATION_OPTIONS, null, null, this._undoRedoService);
|
||||
|
||||
// tree
|
||||
this._treeContainer = dom.append(containerElement, dom.$('div.ref-tree.inline'));
|
||||
const treeOptions: IWorkbenchAsyncDataTreeOptions<TreeElement, FuzzyScore> = {
|
||||
keyboardSupport: this._defaultTreeKeyboardSupport,
|
||||
accessibilityProvider: new AccessibilityProvider(),
|
||||
keyboardNavigationLabelProvider: this._instantiationService.createInstance(StringRepresentationProvider),
|
||||
identityProvider: new IdentityProvider(),
|
||||
openOnSingleClick: true,
|
||||
openOnFocus: true,
|
||||
overrideStyles: {
|
||||
listBackground: peekView.peekViewResultsBackground
|
||||
}
|
||||
};
|
||||
if (this._defaultTreeKeyboardSupport) {
|
||||
// the tree will consume `Escape` and prevent the widget from closing
|
||||
this._callOnDispose.add(dom.addStandardDisposableListener(this._treeContainer, 'keydown', (e) => {
|
||||
if (e.equals(KeyCode.Escape)) {
|
||||
this._keybindingService.dispatchEvent(e, e.target);
|
||||
e.stopPropagation();
|
||||
}
|
||||
}, true));
|
||||
}
|
||||
this._tree = this._instantiationService.createInstance(
|
||||
ReferencesTree,
|
||||
'ReferencesWidget',
|
||||
this._treeContainer,
|
||||
new Delegate(),
|
||||
[
|
||||
this._instantiationService.createInstance(FileReferencesRenderer),
|
||||
this._instantiationService.createInstance(OneReferenceRenderer),
|
||||
],
|
||||
this._instantiationService.createInstance(DataSource),
|
||||
treeOptions,
|
||||
);
|
||||
|
||||
// split stuff
|
||||
this._splitView.addView({
|
||||
onDidChange: Event.None,
|
||||
element: this._previewContainer,
|
||||
minimumSize: 200,
|
||||
maximumSize: Number.MAX_VALUE,
|
||||
layout: (width) => {
|
||||
this._preview.layout({ height: this._dim.height, width });
|
||||
}
|
||||
}, Sizing.Distribute);
|
||||
|
||||
this._splitView.addView({
|
||||
onDidChange: Event.None,
|
||||
element: this._treeContainer,
|
||||
minimumSize: 100,
|
||||
maximumSize: Number.MAX_VALUE,
|
||||
layout: (width) => {
|
||||
this._treeContainer.style.height = `${this._dim.height}px`;
|
||||
this._treeContainer.style.width = `${width}px`;
|
||||
this._tree.layout(this._dim.height, width);
|
||||
}
|
||||
}, Sizing.Distribute);
|
||||
|
||||
this._disposables.add(this._splitView.onDidSashChange(() => {
|
||||
if (this._dim.width) {
|
||||
this.layoutData.ratio = this._splitView.getViewSize(0) / this._dim.width;
|
||||
}
|
||||
}, undefined));
|
||||
|
||||
// listen on selection and focus
|
||||
let onEvent = (element: any, kind: 'show' | 'goto' | 'side') => {
|
||||
if (element instanceof OneReference) {
|
||||
if (kind === 'show') {
|
||||
this._revealReference(element, false);
|
||||
}
|
||||
this._onDidSelectReference.fire({ element, kind, source: 'tree' });
|
||||
}
|
||||
};
|
||||
this._tree.onDidOpen(e => {
|
||||
if (e.sideBySide) {
|
||||
onEvent(e.element, 'side');
|
||||
} else if (e.editorOptions.pinned) {
|
||||
onEvent(e.element, 'goto');
|
||||
} else {
|
||||
onEvent(e.element, 'show');
|
||||
}
|
||||
});
|
||||
|
||||
dom.hide(this._treeContainer);
|
||||
}
|
||||
|
||||
protected _onWidth(width: number) {
|
||||
if (this._dim) {
|
||||
this._doLayoutBody(this._dim.height, width);
|
||||
}
|
||||
}
|
||||
|
||||
protected _doLayoutBody(heightInPixel: number, widthInPixel: number): void {
|
||||
super._doLayoutBody(heightInPixel, widthInPixel);
|
||||
this._dim = new dom.Dimension(widthInPixel, heightInPixel);
|
||||
this.layoutData.heightInLines = this._viewZone ? this._viewZone.heightInLines : this.layoutData.heightInLines;
|
||||
this._splitView.layout(widthInPixel);
|
||||
this._splitView.resizeView(0, widthInPixel * this.layoutData.ratio);
|
||||
}
|
||||
|
||||
setSelection(selection: OneReference): Promise<any> {
|
||||
return this._revealReference(selection, true).then(() => {
|
||||
if (!this._model) {
|
||||
// disposed
|
||||
return;
|
||||
}
|
||||
// show in tree
|
||||
this._tree.setSelection([selection]);
|
||||
this._tree.setFocus([selection]);
|
||||
});
|
||||
}
|
||||
|
||||
setModel(newModel: ReferencesModel | undefined): Promise<any> {
|
||||
// clean up
|
||||
this._disposeOnNewModel.clear();
|
||||
this._model = newModel;
|
||||
if (this._model) {
|
||||
return this._onNewModel();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private _onNewModel(): Promise<any> {
|
||||
if (!this._model) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
if (this._model.isEmpty) {
|
||||
this.setTitle('');
|
||||
this._messageContainer.innerText = nls.localize('noResults', "No results");
|
||||
dom.show(this._messageContainer);
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
dom.hide(this._messageContainer);
|
||||
this._decorationsManager = new DecorationsManager(this._preview, this._model);
|
||||
this._disposeOnNewModel.add(this._decorationsManager);
|
||||
|
||||
// listen on model changes
|
||||
this._disposeOnNewModel.add(this._model.onDidChangeReferenceRange(reference => this._tree.rerender(reference)));
|
||||
|
||||
// listen on editor
|
||||
this._disposeOnNewModel.add(this._preview.onMouseDown(e => {
|
||||
const { event, target } = e;
|
||||
if (event.detail !== 2) {
|
||||
return;
|
||||
}
|
||||
const element = this._getFocusedReference();
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
this._onDidSelectReference.fire({
|
||||
element: { uri: element.uri, range: target.range! },
|
||||
kind: (event.ctrlKey || event.metaKey || event.altKey) ? 'side' : 'open',
|
||||
source: 'editor'
|
||||
});
|
||||
}));
|
||||
|
||||
// make sure things are rendered
|
||||
this.container!.classList.add('results-loaded');
|
||||
dom.show(this._treeContainer);
|
||||
dom.show(this._previewContainer);
|
||||
this._splitView.layout(this._dim.width);
|
||||
this.focusOnReferenceTree();
|
||||
|
||||
// pick input and a reference to begin with
|
||||
return this._tree.setInput(this._model.groups.length === 1 ? this._model.groups[0] : this._model);
|
||||
}
|
||||
|
||||
private _getFocusedReference(): OneReference | undefined {
|
||||
const [element] = this._tree.getFocus();
|
||||
if (element instanceof OneReference) {
|
||||
return element;
|
||||
} else if (element instanceof FileReferences) {
|
||||
if (element.children.length > 0) {
|
||||
return element.children[0];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async revealReference(reference: OneReference): Promise<void> {
|
||||
await this._revealReference(reference, false);
|
||||
this._onDidSelectReference.fire({ element: reference, kind: 'goto', source: 'tree' });
|
||||
}
|
||||
|
||||
private _revealedReference?: OneReference;
|
||||
|
||||
private async _revealReference(reference: OneReference, revealParent: boolean): Promise<void> {
|
||||
|
||||
// check if there is anything to do...
|
||||
if (this._revealedReference === reference) {
|
||||
return;
|
||||
}
|
||||
this._revealedReference = reference;
|
||||
|
||||
// Update widget header
|
||||
if (reference.uri.scheme !== Schemas.inMemory) {
|
||||
this.setTitle(basenameOrAuthority(reference.uri), this._uriLabel.getUriLabel(dirname(reference.uri)));
|
||||
} else {
|
||||
this.setTitle(nls.localize('peekView.alternateTitle', "References"));
|
||||
}
|
||||
|
||||
const promise = this._textModelResolverService.createModelReference(reference.uri);
|
||||
|
||||
if (this._tree.getInput() === reference.parent) {
|
||||
this._tree.reveal(reference);
|
||||
} else {
|
||||
if (revealParent) {
|
||||
this._tree.reveal(reference.parent);
|
||||
}
|
||||
await this._tree.expand(reference.parent);
|
||||
this._tree.reveal(reference);
|
||||
}
|
||||
|
||||
const ref = await promise;
|
||||
|
||||
if (!this._model) {
|
||||
// disposed
|
||||
ref.dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
dispose(this._previewModelReference);
|
||||
|
||||
// show in editor
|
||||
const model = ref.object;
|
||||
if (model) {
|
||||
const scrollType = this._preview.getModel() === model.textEditorModel ? ScrollType.Smooth : ScrollType.Immediate;
|
||||
const sel = Range.lift(reference.range).collapseToStart();
|
||||
this._previewModelReference = ref;
|
||||
this._preview.setModel(model.textEditorModel);
|
||||
this._preview.setSelection(sel);
|
||||
this._preview.revealRangeInCenter(sel, scrollType);
|
||||
} else {
|
||||
this._preview.setModel(this._previewNotAvailableMessage);
|
||||
ref.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// theming
|
||||
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
const findMatchHighlightColor = theme.getColor(peekView.peekViewResultsMatchHighlight);
|
||||
if (findMatchHighlightColor) {
|
||||
collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .referenceMatch .highlight { background-color: ${findMatchHighlightColor}; }`);
|
||||
}
|
||||
const referenceHighlightColor = theme.getColor(peekView.peekViewEditorMatchHighlight);
|
||||
if (referenceHighlightColor) {
|
||||
collector.addRule(`.monaco-editor .reference-zone-widget .preview .reference-decoration { background-color: ${referenceHighlightColor}; }`);
|
||||
}
|
||||
const referenceHighlightBorder = theme.getColor(peekView.peekViewEditorMatchHighlightBorder);
|
||||
if (referenceHighlightBorder) {
|
||||
collector.addRule(`.monaco-editor .reference-zone-widget .preview .reference-decoration { border: 2px solid ${referenceHighlightBorder}; box-sizing: border-box; }`);
|
||||
}
|
||||
const hcOutline = theme.getColor(activeContrastBorder);
|
||||
if (hcOutline) {
|
||||
collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .referenceMatch .highlight { border: 1px dotted ${hcOutline}; box-sizing: border-box; }`);
|
||||
}
|
||||
const resultsBackground = theme.getColor(peekView.peekViewResultsBackground);
|
||||
if (resultsBackground) {
|
||||
collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree { background-color: ${resultsBackground}; }`);
|
||||
}
|
||||
const resultsMatchForeground = theme.getColor(peekView.peekViewResultsMatchForeground);
|
||||
if (resultsMatchForeground) {
|
||||
collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree { color: ${resultsMatchForeground}; }`);
|
||||
}
|
||||
const resultsFileForeground = theme.getColor(peekView.peekViewResultsFileForeground);
|
||||
if (resultsFileForeground) {
|
||||
collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .reference-file { color: ${resultsFileForeground}; }`);
|
||||
}
|
||||
const resultsSelectedBackground = theme.getColor(peekView.peekViewResultsSelectionBackground);
|
||||
if (resultsSelectedBackground) {
|
||||
collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { background-color: ${resultsSelectedBackground}; }`);
|
||||
}
|
||||
const resultsSelectedForeground = theme.getColor(peekView.peekViewResultsSelectionForeground);
|
||||
if (resultsSelectedForeground) {
|
||||
collector.addRule(`.monaco-editor .reference-zone-widget .ref-tree .monaco-list:focus .monaco-list-rows > .monaco-list-row.selected:not(.highlighted) { color: ${resultsSelectedForeground} !important; }`);
|
||||
}
|
||||
const editorBackground = theme.getColor(peekView.peekViewEditorBackground);
|
||||
if (editorBackground) {
|
||||
collector.addRule(
|
||||
`.monaco-editor .reference-zone-widget .preview .monaco-editor .monaco-editor-background,` +
|
||||
`.monaco-editor .reference-zone-widget .preview .monaco-editor .inputarea.ime-input {` +
|
||||
` background-color: ${editorBackground};` +
|
||||
`}`);
|
||||
}
|
||||
const editorGutterBackground = theme.getColor(peekView.peekViewEditorGutterBackground);
|
||||
if (editorGutterBackground) {
|
||||
collector.addRule(
|
||||
`.monaco-editor .reference-zone-widget .preview .monaco-editor .margin {` +
|
||||
` background-color: ${editorGutterBackground};` +
|
||||
`}`);
|
||||
}
|
||||
});
|
||||
286
lib/vscode/src/vs/editor/contrib/gotoSymbol/referencesModel.ts
Normal file
286
lib/vscode/src/vs/editor/contrib/gotoSymbol/referencesModel.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { localize } from 'vs/nls';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { basename, extUri } from 'vs/base/common/resources';
|
||||
import { IDisposable, dispose, IReference, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { defaultGenerator } from 'vs/base/common/idGenerator';
|
||||
import { Range, IRange } from 'vs/editor/common/core/range';
|
||||
import { Location, LocationLink } from 'vs/editor/common/modes';
|
||||
import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { IMatch } from 'vs/base/common/filters';
|
||||
import { Constants } from 'vs/base/common/uint';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
export class OneReference {
|
||||
|
||||
readonly id: string = defaultGenerator.nextId();
|
||||
|
||||
constructor(
|
||||
readonly isProviderFirst: boolean,
|
||||
readonly parent: FileReferences,
|
||||
readonly uri: URI,
|
||||
private _range: IRange,
|
||||
private _rangeCallback: (ref: OneReference) => void
|
||||
) { }
|
||||
|
||||
get range(): IRange {
|
||||
return this._range;
|
||||
}
|
||||
|
||||
set range(value: IRange) {
|
||||
this._range = value;
|
||||
this._rangeCallback(this);
|
||||
}
|
||||
|
||||
get ariaMessage(): string {
|
||||
return localize(
|
||||
'aria.oneReference', "symbol in {0} on line {1} at column {2}",
|
||||
basename(this.uri), this.range.startLineNumber, this.range.startColumn
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class FilePreview implements IDisposable {
|
||||
|
||||
constructor(
|
||||
private readonly _modelReference: IReference<ITextEditorModel>
|
||||
) { }
|
||||
|
||||
dispose(): void {
|
||||
this._modelReference.dispose();
|
||||
}
|
||||
|
||||
preview(range: IRange, n: number = 8): { value: string; highlight: IMatch } | undefined {
|
||||
const model = this._modelReference.object.textEditorModel;
|
||||
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { startLineNumber, startColumn, endLineNumber, endColumn } = range;
|
||||
const word = model.getWordUntilPosition({ lineNumber: startLineNumber, column: startColumn - n });
|
||||
const beforeRange = new Range(startLineNumber, word.startColumn, startLineNumber, startColumn);
|
||||
const afterRange = new Range(endLineNumber, endColumn, endLineNumber, Constants.MAX_SAFE_SMALL_INTEGER);
|
||||
|
||||
const before = model.getValueInRange(beforeRange).replace(/^\s+/, '');
|
||||
const inside = model.getValueInRange(range);
|
||||
const after = model.getValueInRange(afterRange).replace(/\s+$/, '');
|
||||
|
||||
return {
|
||||
value: before + inside + after,
|
||||
highlight: { start: before.length, end: before.length + inside.length }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class FileReferences implements IDisposable {
|
||||
|
||||
readonly children: OneReference[] = [];
|
||||
|
||||
private _previews = new ResourceMap<FilePreview>();
|
||||
|
||||
constructor(
|
||||
readonly parent: ReferencesModel,
|
||||
readonly uri: URI
|
||||
) { }
|
||||
|
||||
dispose(): void {
|
||||
dispose(this._previews.values());
|
||||
this._previews.clear();
|
||||
}
|
||||
|
||||
getPreview(child: OneReference): FilePreview | undefined {
|
||||
return this._previews.get(child.uri);
|
||||
}
|
||||
|
||||
get ariaMessage(): string {
|
||||
const len = this.children.length;
|
||||
if (len === 1) {
|
||||
return localize('aria.fileReferences.1', "1 symbol in {0}, full path {1}", basename(this.uri), this.uri.fsPath);
|
||||
} else {
|
||||
return localize('aria.fileReferences.N', "{0} symbols in {1}, full path {2}", len, basename(this.uri), this.uri.fsPath);
|
||||
}
|
||||
}
|
||||
|
||||
async resolve(textModelResolverService: ITextModelService): Promise<FileReferences> {
|
||||
if (this._previews.size !== 0) {
|
||||
return this;
|
||||
}
|
||||
for (let child of this.children) {
|
||||
if (this._previews.has(child.uri)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const ref = await textModelResolverService.createModelReference(child.uri);
|
||||
this._previews.set(child.uri, new FilePreview(ref));
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class ReferencesModel implements IDisposable {
|
||||
|
||||
private readonly _disposables = new DisposableStore();
|
||||
private readonly _links: LocationLink[];
|
||||
private readonly _title: string;
|
||||
|
||||
readonly groups: FileReferences[] = [];
|
||||
readonly references: OneReference[] = [];
|
||||
|
||||
readonly _onDidChangeReferenceRange = new Emitter<OneReference>();
|
||||
readonly onDidChangeReferenceRange: Event<OneReference> = this._onDidChangeReferenceRange.event;
|
||||
|
||||
constructor(links: LocationLink[], title: string) {
|
||||
this._links = links;
|
||||
this._title = title;
|
||||
|
||||
// grouping and sorting
|
||||
const [providersFirst] = links;
|
||||
links.sort(ReferencesModel._compareReferences);
|
||||
|
||||
let current: FileReferences | undefined;
|
||||
for (let link of links) {
|
||||
if (!current || !extUri.isEqual(current.uri, link.uri, true)) {
|
||||
// new group
|
||||
current = new FileReferences(this, link.uri);
|
||||
this.groups.push(current);
|
||||
}
|
||||
|
||||
// append, check for equality first!
|
||||
if (current.children.length === 0 || ReferencesModel._compareReferences(link, current.children[current.children.length - 1]) !== 0) {
|
||||
|
||||
const oneRef = new OneReference(
|
||||
providersFirst === link,
|
||||
current,
|
||||
link.uri,
|
||||
link.targetSelectionRange || link.range,
|
||||
ref => this._onDidChangeReferenceRange.fire(ref)
|
||||
);
|
||||
this.references.push(oneRef);
|
||||
current.children.push(oneRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this.groups);
|
||||
this._disposables.dispose();
|
||||
this._onDidChangeReferenceRange.dispose();
|
||||
this.groups.length = 0;
|
||||
}
|
||||
|
||||
clone(): ReferencesModel {
|
||||
return new ReferencesModel(this._links, this._title);
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this._title;
|
||||
}
|
||||
|
||||
get isEmpty(): boolean {
|
||||
return this.groups.length === 0;
|
||||
}
|
||||
|
||||
get ariaMessage(): string {
|
||||
if (this.isEmpty) {
|
||||
return localize('aria.result.0', "No results found");
|
||||
} else if (this.references.length === 1) {
|
||||
return localize('aria.result.1', "Found 1 symbol in {0}", this.references[0].uri.fsPath);
|
||||
} else if (this.groups.length === 1) {
|
||||
return localize('aria.result.n1', "Found {0} symbols in {1}", this.references.length, this.groups[0].uri.fsPath);
|
||||
} else {
|
||||
return localize('aria.result.nm', "Found {0} symbols in {1} files", this.references.length, this.groups.length);
|
||||
}
|
||||
}
|
||||
|
||||
nextOrPreviousReference(reference: OneReference, next: boolean): OneReference {
|
||||
|
||||
let { parent } = reference;
|
||||
|
||||
let idx = parent.children.indexOf(reference);
|
||||
let childCount = parent.children.length;
|
||||
let groupCount = parent.parent.groups.length;
|
||||
|
||||
if (groupCount === 1 || next && idx + 1 < childCount || !next && idx > 0) {
|
||||
// cycling within one file
|
||||
if (next) {
|
||||
idx = (idx + 1) % childCount;
|
||||
} else {
|
||||
idx = (idx + childCount - 1) % childCount;
|
||||
}
|
||||
return parent.children[idx];
|
||||
}
|
||||
|
||||
idx = parent.parent.groups.indexOf(parent);
|
||||
if (next) {
|
||||
idx = (idx + 1) % groupCount;
|
||||
return parent.parent.groups[idx].children[0];
|
||||
} else {
|
||||
idx = (idx + groupCount - 1) % groupCount;
|
||||
return parent.parent.groups[idx].children[parent.parent.groups[idx].children.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
nearestReference(resource: URI, position: Position): OneReference | undefined {
|
||||
|
||||
const nearest = this.references.map((ref, idx) => {
|
||||
return {
|
||||
idx,
|
||||
prefixLen: strings.commonPrefixLength(ref.uri.toString(), resource.toString()),
|
||||
offsetDist: Math.abs(ref.range.startLineNumber - position.lineNumber) * 100 + Math.abs(ref.range.startColumn - position.column)
|
||||
};
|
||||
}).sort((a, b) => {
|
||||
if (a.prefixLen > b.prefixLen) {
|
||||
return -1;
|
||||
} else if (a.prefixLen < b.prefixLen) {
|
||||
return 1;
|
||||
} else if (a.offsetDist < b.offsetDist) {
|
||||
return -1;
|
||||
} else if (a.offsetDist > b.offsetDist) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
})[0];
|
||||
|
||||
if (nearest) {
|
||||
return this.references[nearest.idx];
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
referenceAt(resource: URI, position: Position): OneReference | undefined {
|
||||
for (const ref of this.references) {
|
||||
if (ref.uri.toString() === resource.toString()) {
|
||||
if (Range.containsPosition(ref.range, position)) {
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
firstReference(): OneReference | undefined {
|
||||
for (const ref of this.references) {
|
||||
if (ref.isProviderFirst) {
|
||||
return ref;
|
||||
}
|
||||
}
|
||||
return this.references[0];
|
||||
}
|
||||
|
||||
private static _compareReferences(a: Location, b: Location): number {
|
||||
return extUri.compare(a.uri, b.uri) || Range.compareRangesUsingStarts(a.range, b.range);
|
||||
}
|
||||
}
|
||||
215
lib/vscode/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts
Normal file
215
lib/vscode/src/vs/editor/contrib/gotoSymbol/symbolNavigation.ts
Normal file
@@ -0,0 +1,215 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ReferencesModel, OneReference } from 'vs/editor/contrib/gotoSymbol/referencesModel';
|
||||
import { RawContextKey, IContextKeyService, IContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { createDecorator, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
import { KeybindingWeight, KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { registerEditorCommand, EditorCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { dispose, IDisposable, combinedDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
import { TextEditorSelectionRevealType } from 'vs/platform/editor/common/editor';
|
||||
|
||||
export const ctxHasSymbols = new RawContextKey('hasSymbols', false);
|
||||
|
||||
export const ISymbolNavigationService = createDecorator<ISymbolNavigationService>('ISymbolNavigationService');
|
||||
|
||||
export interface ISymbolNavigationService {
|
||||
readonly _serviceBrand: undefined;
|
||||
reset(): void;
|
||||
put(anchor: OneReference): void;
|
||||
revealNext(source: ICodeEditor): Promise<any>;
|
||||
}
|
||||
|
||||
class SymbolNavigationService implements ISymbolNavigationService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _ctxHasSymbols: IContextKey<boolean>;
|
||||
|
||||
private _currentModel?: ReferencesModel = undefined;
|
||||
private _currentIdx: number = -1;
|
||||
private _currentState?: IDisposable;
|
||||
private _currentMessage?: IDisposable;
|
||||
private _ignoreEditorChange: boolean = false;
|
||||
|
||||
constructor(
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@ICodeEditorService private readonly _editorService: ICodeEditorService,
|
||||
@INotificationService private readonly _notificationService: INotificationService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
) {
|
||||
this._ctxHasSymbols = ctxHasSymbols.bindTo(contextKeyService);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._ctxHasSymbols.reset();
|
||||
this._currentState?.dispose();
|
||||
this._currentMessage?.dispose();
|
||||
this._currentModel = undefined;
|
||||
this._currentIdx = -1;
|
||||
}
|
||||
|
||||
put(anchor: OneReference): void {
|
||||
const refModel = anchor.parent.parent;
|
||||
|
||||
if (refModel.references.length <= 1) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
this._currentModel = refModel;
|
||||
this._currentIdx = refModel.references.indexOf(anchor);
|
||||
this._ctxHasSymbols.set(true);
|
||||
this._showMessage();
|
||||
|
||||
const editorState = new EditorState(this._editorService);
|
||||
const listener = editorState.onDidChange(_ => {
|
||||
|
||||
if (this._ignoreEditorChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editor = this._editorService.getActiveCodeEditor();
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const model = editor.getModel();
|
||||
const position = editor.getPosition();
|
||||
if (!model || !position) {
|
||||
return;
|
||||
}
|
||||
|
||||
let seenUri: boolean = false;
|
||||
let seenPosition: boolean = false;
|
||||
for (const reference of refModel.references) {
|
||||
if (isEqual(reference.uri, model.uri)) {
|
||||
seenUri = true;
|
||||
seenPosition = seenPosition || Range.containsPosition(reference.range, position);
|
||||
} else if (seenUri) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!seenUri || !seenPosition) {
|
||||
this.reset();
|
||||
}
|
||||
});
|
||||
|
||||
this._currentState = combinedDisposable(editorState, listener);
|
||||
}
|
||||
|
||||
revealNext(source: ICodeEditor): Promise<any> {
|
||||
if (!this._currentModel) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// get next result and advance
|
||||
this._currentIdx += 1;
|
||||
this._currentIdx %= this._currentModel.references.length;
|
||||
const reference = this._currentModel.references[this._currentIdx];
|
||||
|
||||
// status
|
||||
this._showMessage();
|
||||
|
||||
// open editor, ignore events while that happens
|
||||
this._ignoreEditorChange = true;
|
||||
return this._editorService.openCodeEditor({
|
||||
resource: reference.uri,
|
||||
options: {
|
||||
selection: Range.collapseToStart(reference.range),
|
||||
selectionRevealType: TextEditorSelectionRevealType.NearTopIfOutsideViewport
|
||||
}
|
||||
}, source).finally(() => {
|
||||
this._ignoreEditorChange = false;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private _showMessage(): void {
|
||||
|
||||
this._currentMessage?.dispose();
|
||||
|
||||
const kb = this._keybindingService.lookupKeybinding('editor.gotoNextSymbolFromResult');
|
||||
const message = kb
|
||||
? localize('location.kb', "Symbol {0} of {1}, {2} for next", this._currentIdx + 1, this._currentModel!.references.length, kb.getLabel())
|
||||
: localize('location', "Symbol {0} of {1}", this._currentIdx + 1, this._currentModel!.references.length);
|
||||
|
||||
this._currentMessage = this._notificationService.status(message);
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ISymbolNavigationService, SymbolNavigationService, true);
|
||||
|
||||
registerEditorCommand(new class extends EditorCommand {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: 'editor.gotoNextSymbolFromResult',
|
||||
precondition: ctxHasSymbols,
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
primary: KeyCode.F12
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise<void> {
|
||||
return accessor.get(ISymbolNavigationService).revealNext(editor);
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'editor.gotoNextSymbolFromResult.cancel',
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
when: ctxHasSymbols,
|
||||
primary: KeyCode.Escape,
|
||||
handler(accessor) {
|
||||
accessor.get(ISymbolNavigationService).reset();
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
class EditorState {
|
||||
|
||||
private readonly _listener = new Map<ICodeEditor, IDisposable>();
|
||||
private readonly _disposables = new DisposableStore();
|
||||
|
||||
private readonly _onDidChange = new Emitter<{ editor: ICodeEditor }>();
|
||||
readonly onDidChange: Event<{ editor: ICodeEditor }> = this._onDidChange.event;
|
||||
|
||||
constructor(@ICodeEditorService editorService: ICodeEditorService) {
|
||||
this._disposables.add(editorService.onCodeEditorRemove(this._onDidRemoveEditor, this));
|
||||
this._disposables.add(editorService.onCodeEditorAdd(this._onDidAddEditor, this));
|
||||
editorService.listCodeEditors().forEach(this._onDidAddEditor, this);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._disposables.dispose();
|
||||
this._onDidChange.dispose();
|
||||
dispose(this._listener.values());
|
||||
}
|
||||
|
||||
private _onDidAddEditor(editor: ICodeEditor): void {
|
||||
this._listener.set(editor, combinedDisposable(
|
||||
editor.onDidChangeCursorPosition(_ => this._onDidChange.fire({ editor })),
|
||||
editor.onDidChangeModelContent(_ => this._onDidChange.fire({ editor })),
|
||||
));
|
||||
}
|
||||
|
||||
private _onDidRemoveEditor(editor: ICodeEditor): void {
|
||||
this._listener.get(editor)?.dispose();
|
||||
this._listener.delete(editor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { ReferencesModel } from 'vs/editor/contrib/gotoSymbol/referencesModel';
|
||||
|
||||
suite('references', function () {
|
||||
|
||||
test('nearestReference', () => {
|
||||
const model = new ReferencesModel([{
|
||||
uri: URI.file('/out/obj/can'),
|
||||
range: new Range(1, 1, 1, 1)
|
||||
}, {
|
||||
uri: URI.file('/out/obj/can2'),
|
||||
range: new Range(1, 1, 1, 1)
|
||||
}, {
|
||||
uri: URI.file('/src/can'),
|
||||
range: new Range(1, 1, 1, 1)
|
||||
}], 'FOO');
|
||||
|
||||
let ref = model.nearestReference(URI.file('/src/can'), new Position(1, 1));
|
||||
assert.equal(ref!.uri.path, '/src/can');
|
||||
|
||||
ref = model.nearestReference(URI.file('/src/someOtherFileInSrc'), new Position(1, 1));
|
||||
assert.equal(ref!.uri.path, '/src/can');
|
||||
|
||||
ref = model.nearestReference(URI.file('/out/someOtherFile'), new Position(1, 1));
|
||||
assert.equal(ref!.uri.path, '/out/obj/can');
|
||||
|
||||
ref = model.nearestReference(URI.file('/out/obj/can2222'), new Position(1, 1));
|
||||
assert.equal(ref!.uri.path, '/out/obj/can2');
|
||||
});
|
||||
|
||||
});
|
||||
36
lib/vscode/src/vs/editor/contrib/hover/getHover.ts
Normal file
36
lib/vscode/src/vs/editor/contrib/hover/getHover.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { coalesce } from 'vs/base/common/arrays';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedExternalError } from 'vs/base/common/errors';
|
||||
import { registerModelAndPositionCommand } from 'vs/editor/browser/editorExtensions';
|
||||
import { Position } from 'vs/editor/common/core/position';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { Hover, HoverProviderRegistry } from 'vs/editor/common/modes';
|
||||
|
||||
export function getHover(model: ITextModel, position: Position, token: CancellationToken): Promise<Hover[]> {
|
||||
|
||||
const supports = HoverProviderRegistry.ordered(model);
|
||||
|
||||
const promises = supports.map(support => {
|
||||
return Promise.resolve(support.provideHover(model, position, token)).then(hover => {
|
||||
return hover && isValid(hover) ? hover : undefined;
|
||||
}, err => {
|
||||
onUnexpectedExternalError(err);
|
||||
return undefined;
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(promises).then(coalesce);
|
||||
}
|
||||
|
||||
registerModelAndPositionCommand('_executeHoverProvider', (model, position) => getHover(model, position, CancellationToken.None));
|
||||
|
||||
function isValid(result: Hover) {
|
||||
const hasRange = (typeof result.range !== 'undefined');
|
||||
const hasHtmlContent = typeof result.contents !== 'undefined' && result.contents && result.contents.length > 0;
|
||||
return hasRange && hasHtmlContent;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user