提交 e7e8ed29 编写于 作者: M Martin Aeschlimann

Merge pull request #2971 from Microsoft/aeschli-folding

Add indent based code folding
......@@ -38,6 +38,7 @@ define([
'vs/editor/contrib/wordHighlighter/common/wordHighlighter',
'vs/editor/contrib/workerStatusReporter/browser/workerStatusReporter',
'vs/editor/contrib/defineKeybinding/browser/defineKeybinding',
"vs/editor/contrib/folding/browser/folding",
// include these in the editor bundle because they are widely used by many languages
'vs/editor/common/languages.common'
......
......@@ -591,6 +591,11 @@ export interface ICodeEditor extends EditorCommon.ICommonCodeEditor {
* Warning: the results of this method are innacurate for positions that are outside the current editor viewport.
*/
getScrolledVisiblePosition(position: EditorCommon.IPosition): { top: number; left: number; height: number; };
/**
* Set the model ranges that will be hidden in the view.
*/
setHiddenAreas(ranges:EditorCommon.IRange[]): void;
}
/**
......
......@@ -181,11 +181,20 @@ export class CodeEditorWidget extends CommonCodeEditor implements EditorBrowser.
if (!this.cursor || !this.hasView) {
return null;
}
let contributionsState: {[key:string]:any} = {};
for (let id in this.contributions) {
let contribution = this.contributions[id];
if (typeof contribution.saveViewState === 'function') {
contributionsState[id] = contribution.saveViewState();
}
}
var cursorState = this.cursor.saveState();
var viewState = this._view.saveState();
return {
cursorState: cursorState,
viewState: viewState
viewState: viewState,
contributionsState: contributionsState
};
}
......@@ -204,6 +213,14 @@ export class CodeEditorWidget extends CommonCodeEditor implements EditorBrowser.
this.cursor.restoreState([<EditorCommon.ICursorState>cursorState]);
}
this._view.restoreState(codeEditorState.viewState);
let contributionsState = s.contributionsState || {};
for (let id in this.contributions) {
let contribution = this.contributions[id];
if (typeof contribution.restoreViewState === 'function') {
contribution.restoreViewState(contributionsState[id]);
}
}
}
}
......@@ -365,6 +382,12 @@ export class CodeEditorWidget extends CommonCodeEditor implements EditorBrowser.
this._view.render(true);
}
public setHiddenAreas(ranges:EditorCommon.IRange[]): void {
if (this.viewModel) {
this.viewModel.setHiddenAreas(ranges);
}
}
_attachModel(model:EditorCommon.IModel): void {
this._view = null;
......
......@@ -114,6 +114,7 @@ function cloneInternalEditorOptions(opts: EditorCommon.IInternalEditorOptions):
selectionHighlight: opts.selectionHighlight,
outlineMarkers: opts.outlineMarkers,
referenceInfos: opts.referenceInfos,
folding: opts.folding,
renderWhitespace: opts.renderWhitespace,
layoutInfo: {
width: opts.layoutInfo.width,
......@@ -210,6 +211,9 @@ class InternalEditorOptionsHelper {
let lineNumbers = opts.lineNumbers;
let lineNumbersMinChars = toInteger(opts.lineNumbersMinChars, 1);
let lineDecorationsWidth = toInteger(opts.lineDecorationsWidth, 0);
if (opts.folding) {
lineDecorationsWidth += (opts.fontSize || 16);
}
let layoutInfo = EditorLayoutProvider.compute({
outerWidth: outerWidth,
outerHeight: outerHeight,
......@@ -301,6 +305,7 @@ class InternalEditorOptionsHelper {
selectionHighlight: toBoolean(opts.selectionHighlight),
outlineMarkers: toBoolean(opts.outlineMarkers),
referenceInfos: toBoolean(opts.referenceInfos),
folding: toBoolean(opts.folding),
renderWhitespace: toBoolean(opts.renderWhitespace),
layoutInfo: layoutInfo,
......@@ -392,6 +397,7 @@ class InternalEditorOptionsHelper {
selectionHighlight: (prevOpts.selectionHighlight !== newOpts.selectionHighlight),
outlineMarkers: (prevOpts.outlineMarkers !== newOpts.outlineMarkers),
referenceInfos: (prevOpts.referenceInfos !== newOpts.referenceInfos),
folding: (prevOpts.folding !== newOpts.folding),
renderWhitespace: (prevOpts.renderWhitespace !== newOpts.renderWhitespace),
layoutInfo: (!EditorLayoutProvider.layoutEqual(prevOpts.layoutInfo, newOpts.layoutInfo)),
......@@ -997,6 +1003,11 @@ configurationRegistry.registerConfiguration({
'default': DefaultConfig.editor.referenceInfos,
'description': nls.localize('referenceInfos', "Controls if the editor shows reference information for the modes that support it")
},
'editor.folding' : {
'type': 'boolean',
'default': DefaultConfig.editor.folding,
'description': nls.localize('folding', "Controls wheter the editor has code folding enabled")
},
'diffEditor.renderSideBySide' : {
'type': 'boolean',
'default': true,
......
......@@ -67,6 +67,7 @@ class ConfigClass implements IConfiguration {
selectionHighlight: true,
outlineMarkers: false,
referenceInfos: true,
folding: true,
renderWhitespace: false,
tabSize: 4,
......
......@@ -494,6 +494,11 @@ export interface IEditorOptions {
* Defaults to true.
*/
referenceInfos?: boolean;
/**
* Enable code folding
* Defaults to true.
*/
folding?: boolean;
/**
* Enable rendering of leading whitespace.
* Defaults to false.
......@@ -625,6 +630,7 @@ export interface IInternalEditorOptions {
selectionHighlight:boolean;
outlineMarkers: boolean;
referenceInfos: boolean;
folding: boolean;
renderWhitespace: boolean;
// ---- Options that are computed
......@@ -712,6 +718,7 @@ export interface IConfigurationChangedEvent {
selectionHighlight: boolean;
outlineMarkers: boolean;
referenceInfos: boolean;
folding: boolean;
renderWhitespace: boolean;
// ---- Options that are computed
......@@ -2303,6 +2310,7 @@ export interface IViewState {
export interface ICodeEditorViewState extends IEditorViewState {
cursorState:ICursorState[];
viewState:IViewState;
contributionsState: {[id:string]:any};
}
/**
......@@ -2994,6 +3002,14 @@ export interface IEditorContribution {
* Dispose this contribution.
*/
dispose(): void;
/**
* Store view state.
*/
saveViewState?(): any;
/**
* Restore view state.
*/
restoreViewState?(state: any): void;
}
export type MarkedString = string | { language: string; value: string };
......
......@@ -5,6 +5,7 @@
'use strict';
import {Position} from 'vs/editor/common/core/position';
import {Range} from 'vs/editor/common/core/range';
import {PrefixSumComputer, IPrefixSumIndexOfResult} from 'vs/editor/common/viewModel/prefixSumComputer';
import {FilteredLineTokens, IdentityFilteredLineTokens} from 'vs/editor/common/viewModel/filteredLineTokens';
import {ILinesCollection} from 'vs/editor/common/viewModel/viewModel';
......@@ -41,6 +42,8 @@ export interface IModel {
}
export interface ISplitLine {
isVisible():boolean;
setVisible(isVisible:boolean):void;
getOutputLineCount(): number;
getOutputLineContent(model: IModel, myLineNumber: number, outputLineIndex: number): string;
getOutputLineMinColumn(model: IModel, myLineNumber: number, outputLineIndex: number): number;
......@@ -52,33 +55,66 @@ export interface ISplitLine {
class IdentitySplitLine implements ISplitLine {
public static INSTANCE = new IdentitySplitLine();
private _isVisible: boolean;
public constructor(isVisible: boolean) {
this._isVisible = isVisible;
}
public isVisible():boolean {
return this._isVisible;
}
public setVisible(isVisible:boolean):void {
this._isVisible = isVisible;
}
public getOutputLineCount(): number {
if (!this._isVisible) {
return 0;
}
return 1;
}
public getOutputLineContent(model:IModel, myLineNumber:number, outputLineIndex:number): string {
if (!this._isVisible) {
throw new Error('Not supported');
}
return model.getLineContent(myLineNumber);
}
public getOutputLineMinColumn(model: IModel, myLineNumber: number, outputLineIndex: number): number {
if (!this._isVisible) {
throw new Error('Not supported');
}
return model.getLineMinColumn(myLineNumber);
}
public getOutputLineMaxColumn(model:IModel, myLineNumber:number, outputLineIndex:number): number {
if (!this._isVisible) {
throw new Error('Not supported');
}
return model.getLineMaxColumn(myLineNumber);
}
public getOutputLineTokens(model:IModel, myLineNumber:number, outputLineIndex:number, inaccurateTokensAcceptable:boolean): EditorCommon.IViewLineTokens {
if (!this._isVisible) {
throw new Error('Not supported');
}
return new IdentityFilteredLineTokens(model.getLineTokens(myLineNumber, inaccurateTokensAcceptable), model.getLineMaxColumn(myLineNumber) - 1);
}
public getInputColumnOfOutputPosition(outputLineIndex:number, outputColumn:number): number {
if (!this._isVisible) {
throw new Error('Not supported');
}
return outputColumn;
}
public getOutputPositionOfInputPosition(deltaLineNumber:number, inputColumn:number): EditorCommon.IEditorPosition {
if (!this._isVisible) {
throw new Error('Not supported');
}
return new Position(deltaLineNumber, inputColumn);
}
}
......@@ -90,15 +126,28 @@ export class SplitLine implements ISplitLine {
private wrappedIndent:string;
private wrappedIndentLength:number;
private _isVisible: boolean;
constructor(positionMapper:ILineMapping) {
constructor(positionMapper:ILineMapping, isVisible: boolean) {
this.positionMapper = positionMapper;
this.wrappedIndent = this.positionMapper.getWrappedLinesIndent();
this.wrappedIndentLength = this.wrappedIndent.length;
this.outputLineCount = this.positionMapper.getOutputLineCount();
this._isVisible = isVisible;
}
public isVisible():boolean {
return this._isVisible;
}
public setVisible(isVisible:boolean):void {
this._isVisible = isVisible;
}
public getOutputLineCount(): number {
if (!this._isVisible) {
return 0;
}
return this.outputLineCount;
}
......@@ -114,6 +163,9 @@ export class SplitLine implements ISplitLine {
}
public getOutputLineContent(model:IModel, myLineNumber:number, outputLineIndex:number): string {
if (!this._isVisible) {
throw new Error('Not supported');
}
var startOffset = this.getInputStartOffsetOfOutputLineIndex(outputLineIndex);
var endOffset = this.getInputEndOffsetOfOutputLineIndex(model, myLineNumber, outputLineIndex);
var r = model.getLineContent(myLineNumber).substring(startOffset, endOffset);
......@@ -127,6 +179,9 @@ export class SplitLine implements ISplitLine {
public getOutputLineMinColumn(model:IModel, myLineNumber:number, outputLineIndex:number): number {
if (!this._isVisible) {
throw new Error('Not supported');
}
if (outputLineIndex > 0) {
return this.wrappedIndentLength + 1;
}
......@@ -134,10 +189,16 @@ export class SplitLine implements ISplitLine {
}
public getOutputLineMaxColumn(model:IModel, myLineNumber:number, outputLineIndex:number): number {
if (!this._isVisible) {
throw new Error('Not supported');
}
return this.getOutputLineContent(model, myLineNumber, outputLineIndex).length + 1;
}
public getOutputLineTokens(model:IModel, myLineNumber:number, outputLineIndex:number, inaccurateTokensAcceptable:boolean): EditorCommon.IViewLineTokens {
if (!this._isVisible) {
throw new Error('Not supported');
}
var startOffset = this.getInputStartOffsetOfOutputLineIndex(outputLineIndex);
var endOffset = this.getInputEndOffsetOfOutputLineIndex(model, myLineNumber, outputLineIndex);
var deltaStartIndex = 0;
......@@ -148,6 +209,9 @@ export class SplitLine implements ISplitLine {
}
public getInputColumnOfOutputPosition(outputLineIndex:number, outputColumn:number): number {
if (!this._isVisible) {
throw new Error('Not supported');
}
var adjustedColumn = outputColumn - 1;
if (outputLineIndex > 0) {
if (adjustedColumn < this.wrappedIndentLength) {
......@@ -160,6 +224,9 @@ export class SplitLine implements ISplitLine {
}
public getOutputPositionOfInputPosition(deltaLineNumber:number, inputColumn:number): EditorCommon.IEditorPosition {
if (!this._isVisible) {
throw new Error('Not supported');
}
this.positionMapper.getOutputPositionOfInputOffset(inputColumn - 1, tmpOutputPosition);
var outputLineIndex = tmpOutputPosition.outputLineIndex;
var outputColumn = tmpOutputPosition.outputOffset + 1;
......@@ -173,13 +240,13 @@ export class SplitLine implements ISplitLine {
}
}
function createSplitLine(linePositionMapperFactory:ILineMapperFactory, text:string, tabSize:number, wrappingColumn:number, columnsForFullWidthChar:number, wrappingIndent:EditorCommon.WrappingIndent): ISplitLine {
function createSplitLine(linePositionMapperFactory:ILineMapperFactory, text:string, tabSize:number, wrappingColumn:number, columnsForFullWidthChar:number, wrappingIndent:EditorCommon.WrappingIndent, isVisible: boolean): ISplitLine {
var positionMapper = linePositionMapperFactory.createLineMapping(text, tabSize, wrappingColumn, columnsForFullWidthChar, wrappingIndent);
if (positionMapper === null) {
// No mapping needed
return IdentitySplitLine.INSTANCE;
return new IdentitySplitLine(isVisible);
} else {
return new SplitLine(positionMapper);
return new SplitLine(positionMapper, isVisible);
}
}
......@@ -197,6 +264,7 @@ export class SplitLinesCollection implements ILinesCollection {
private linePositionMapperFactory:ILineMapperFactory;
private tmpIndexOfResult: IPrefixSumIndexOfResult;
private hiddenAreasIds:string[];
constructor(model:EditorCommon.IModel, linePositionMapperFactory:ILineMapperFactory, tabSize:number, wrappingColumn:number, columnsForFullWidthChar:number, wrappingIndent:EditorCommon.WrappingIndent) {
this.model = model;
......@@ -215,6 +283,10 @@ export class SplitLinesCollection implements ILinesCollection {
};
}
public dispose(): void {
this.hiddenAreasIds = this.model.deltaDecorations(this.hiddenAreasIds, []);
}
private _ensureValidState(): void {
var modelVersion = this.model.getVersionId();
if (modelVersion !== this._validModelVersionId) {
......@@ -224,13 +296,14 @@ export class SplitLinesCollection implements ILinesCollection {
private constructLines(): void {
this.lines = [];
this.hiddenAreasIds = [];
var line:ISplitLine,
values:number[] = [],
linesContent = this.model.getLinesContent();
for (var i = 0, lineCount = linesContent.length; i < lineCount; i++) {
line = createSplitLine(this.linePositionMapperFactory, linesContent[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent);
line = createSplitLine(this.linePositionMapperFactory, linesContent[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, true);
values[i] = line.getOutputLineCount();
this.lines[i] = line;
}
......@@ -240,6 +313,89 @@ export class SplitLinesCollection implements ILinesCollection {
this.prefixSumComputer = new PrefixSumComputer(values);
}
private getHiddenAreas(): EditorCommon.IEditorRange[] {
return this.hiddenAreasIds.map((decId) => {
return this.model.getDecorationRange(decId);
}).sort(Range.compareRangesUsingStarts);
}
private _reduceRanges(_ranges:EditorCommon.IRange[]): EditorCommon.IEditorRange[] {
if (_ranges.length === 0) {
return [];
}
let ranges = _ranges.map(r => this.model.validateRange(r)).sort(Range.compareRangesUsingStarts);
let result: Range[] = [];
let currentRangeStart = ranges[0].startLineNumber;
let currentRangeEnd = ranges[0].endLineNumber;
for (let i = 1, len = ranges.length; i < len; i++) {
let range = ranges[i];
if (range.startLineNumber > currentRangeEnd + 1) {
result.push(new Range(currentRangeStart, 1, currentRangeEnd, 1));
currentRangeStart = range.startLineNumber;
currentRangeEnd = range.endLineNumber;
} else if (range.endLineNumber > currentRangeEnd) {
currentRangeEnd = range.endLineNumber;
}
}
result.push(new Range(currentRangeStart, 1, currentRangeEnd, 1));
return result;
}
public setHiddenAreas(_ranges:EditorCommon.IRange[], emit:(evenType:string, payload:any)=>void): void {
let ranges = this._reduceRanges(_ranges);
var newDecorations:EditorCommon.IModelDeltaDecoration[] = [];
for (var i = 0; i < ranges.length; i++) {
newDecorations.push({
range: ranges[i],
options: {
}
});
}
this.hiddenAreasIds = this.model.deltaDecorations(this.hiddenAreasIds, newDecorations);
var hiddenAreas = ranges;
var hiddenAreaStart = 1, hiddenAreaEnd = 0;
var hiddenAreaIdx = -1;
var nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.lines.length + 2;
for (var i = 0; i < this.lines.length; i++) {
var lineNumber = i + 1;
if (lineNumber === nextLineNumberToUpdateHiddenArea) {
hiddenAreaIdx++;
hiddenAreaStart = hiddenAreas[hiddenAreaIdx].startLineNumber;
hiddenAreaEnd = hiddenAreas[hiddenAreaIdx].endLineNumber;
nextLineNumberToUpdateHiddenArea = (hiddenAreaIdx + 1 < hiddenAreas.length) ? hiddenAreaEnd + 1 : this.lines.length + 2;
}
var lineChanged = false;
if (lineNumber >= hiddenAreaStart && lineNumber <= hiddenAreaEnd) {
// Line should be hidden
if (this.lines[i].isVisible()) {
this.lines[i].setVisible(false);
lineChanged = true;
}
} else {
// Line should be visible
if (!this.lines[i].isVisible()) {
this.lines[i].setVisible(true);
lineChanged = true;
}
}
if (lineChanged) {
var newOutputLineCount = this.lines[i].getOutputLineCount();
this.prefixSumComputer.changeValue(i, newOutputLineCount);
}
}
emit(EditorCommon.ViewEventNames.ModelFlushedEvent, null);
}
public setTabSize(newTabSize:number, emit:(evenType:string, payload:any)=>void): boolean {
if (this.tabSize === newTabSize) {
return false;
......@@ -286,6 +442,7 @@ export class SplitLinesCollection implements ILinesCollection {
return;
}
this._validModelVersionId = versionId;
var outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1);
var outputToLineNumber = this.prefixSumComputer.getAccumulatedValue(toLineNumber - 1);
......@@ -304,6 +461,17 @@ export class SplitLinesCollection implements ILinesCollection {
return;
}
this._validModelVersionId = versionId;
var hiddenAreas = this.getHiddenAreas();
var isInHiddenArea = false;
var testPosition = new Position(fromLineNumber, 1);
for (var i = 0; i < hiddenAreas.length; i++) {
if (hiddenAreas[i].containsPosition(testPosition)) {
isInHiddenArea = true;
break;
}
}
var outputFromLineNumber = (fromLineNumber === 1 ? 1 : this.prefixSumComputer.getAccumulatedValue(fromLineNumber - 2) + 1);
var line: ISplitLine,
......@@ -314,7 +482,7 @@ export class SplitLinesCollection implements ILinesCollection {
insertPrefixSumValues: number[] = [];
for (var i = 0, len = text.length; i < len; i++) {
var line = createSplitLine(this.linePositionMapperFactory, text[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent);
var line = createSplitLine(this.linePositionMapperFactory, text[i], this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, !isInHiddenArea);
insertLines.push(line);
outputLineCount = line.getOutputLineCount();
......@@ -341,7 +509,8 @@ export class SplitLinesCollection implements ILinesCollection {
var lineIndex = lineNumber - 1;
var oldOutputLineCount = this.lines[lineIndex].getOutputLineCount();
var line = createSplitLine(this.linePositionMapperFactory, newText, this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent);
var isVisible = this.lines[lineIndex].isVisible();
var line = createSplitLine(this.linePositionMapperFactory, newText, this.tabSize, this.wrappingColumn, this.columnsForFullWidthChar, this.wrappingIndent, isVisible);
this.lines[lineIndex] = line;
var newOutputLineCount = this.lines[lineIndex].getOutputLineCount();
......@@ -460,8 +629,25 @@ export class SplitLinesCollection implements ILinesCollection {
if (inputLineNumber > this.lines.length) {
inputLineNumber = this.lines.length;
}
var deltaLineNumber = 1 + (inputLineNumber === 1 ? 0 : this.prefixSumComputer.getAccumulatedValue(inputLineNumber - 2));
var r = this.lines[inputLineNumber - 1].getOutputPositionOfInputPosition(deltaLineNumber, inputColumn);
let lineIndex = inputLineNumber - 1, lineIndexChanged = false;
while (lineIndex > 0 && !this.lines[lineIndex].isVisible()) {
lineIndex--;
lineIndexChanged = true;
}
if (lineIndex === 0 && !this.lines[lineIndex].isVisible()) {
// Could not reach a real line
return new Position(1, 1);
}
var deltaLineNumber = 1 + (lineIndex === 0 ? 0 : this.prefixSumComputer.getAccumulatedValue(lineIndex - 1));
var r:EditorCommon.IEditorPosition;
if (lineIndexChanged) {
r = this.lines[lineIndex].getOutputPositionOfInputPosition(deltaLineNumber, this.model.getLineMaxColumn(lineIndex + 1));
} else {
r = this.lines[inputLineNumber - 1].getOutputPositionOfInputPosition(deltaLineNumber, inputColumn);
}
// console.log('in -> out ' + inputLineNumber + ',' + inputColumn + ' ===> ' + r.lineNumber + ',' + r.column);
return r;
}
......
......@@ -30,6 +30,8 @@ export interface ILinesCollection {
getOutputLineTokens(outputLineNumber:number, inaccurateTokensAcceptable:boolean): EditorCommon.IViewLineTokens;
convertOutputPositionToInputPosition(viewLineNumber:number, viewColumn:number): EditorCommon.IEditorPosition;
convertInputPositionToOutputPosition(inputLineNumber:number, inputColumn:number): EditorCommon.IEditorPosition;
setHiddenAreas(ranges:EditorCommon.IRange[], emit:(evenType:string, payload:any)=>void): void;
dispose(): void;
}
export class ViewModel extends EventEmitter implements EditorCommon.IViewModel {
......@@ -78,6 +80,14 @@ export class ViewModel extends EventEmitter implements EditorCommon.IViewModel {
}));
}
public setHiddenAreas(ranges:EditorCommon.IRange[]): void {
this.deferredEmit(() => {
this.lines.setHiddenAreas(ranges, (eventType:string, payload:any) => this.emit(eventType, payload));
this.decorations.onLineMappingChanged((eventType:string, payload:any) => this.emit(eventType, payload));
this.cursors.onLineMappingChanged((eventType:string, payload:any) => this.emit(eventType, payload));
});
}
public dispose(): void {
this.listenersToRemove.forEach((element) => {
element();
......@@ -86,6 +96,7 @@ export class ViewModel extends EventEmitter implements EditorCommon.IViewModel {
this.listenersToRemove = [];
this.decorations.dispose();
this.decorations = null;
this.lines.dispose();
this.lines = null;
this.configuration = null;
this.model = null;
......
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#7A7A7A" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#7A7A7A" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#646465" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* 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 .folding {
background-repeat: no-repeat;
background-position-y: center;
background-size: 1em 1em;
border-left: 3px solid transparent; /* copy of what the Git decorator does */
margin-left: 5px;
cursor: pointer;
}
.monaco-editor .margin-view-overlays:hover .folding {
background-image: url('arrow-expand.svg');
}
.monaco-editor .margin-view-overlays .folding.collapsed {
background-image: url('arrow-collapse.svg');
}
.monaco-editor.vs-dark .margin-view-overlays:hover .folding {
background-image: url('arrow-expand-dark.svg');
}
.monaco-editor.vs-dark .margin-view-overlays .folding.collapsed {
background-image: url('arrow-collapse-dark.svg');
}
.monaco-editor .inline-folded:after {
color: grey;
margin: 0.1em 0.2em 0 0.2em;
content: "⋯";
display: inline;
line-height: 1em;
cursor: pointer;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <amd-dependency path="vs/css!./folding" />
'use strict';
import {RunOnceScheduler} from 'vs/base/common/async';
import {Range} from 'vs/editor/common/core/range';
import EditorCommon = require('vs/editor/common/editorCommon');
import {IMouseEvent, ICodeEditor} from 'vs/editor/browser/editorBrowser';
import {INullService} from 'vs/platform/instantiation/common/instantiation';
import {IDisposable, disposeAll} from 'vs/base/common/lifecycle';
import Modes = require('vs/editor/common/modes');
import {EditorBrowserRegistry} from 'vs/editor/browser/editorBrowserExtensions';
import {TPromise} from 'vs/base/common/winjs.base';
import foldStrategy = require('vs/editor/contrib/folding/common/indentFoldStrategy');
import {IFoldingRange, toString as rangeToString} from 'vs/editor/contrib/folding/common/foldingRange';
import {CommonEditorRegistry, ContextKey, EditorActionDescriptor} from 'vs/editor/common/editorCommonExtensions';
import {KeyMod, KeyCode} from 'vs/base/common/keyCodes';
import {EditorAction, Behaviour} from 'vs/editor/common/editorAction';
import nls = require('vs/nls');
let log = function(msg: string) {
//console.log(msg);
};
class CollapsibleRegion {
private decorationIds: string[];
private _isCollapsed: boolean;
private _lastRange: IFoldingRange;
public constructor(range:IFoldingRange, model:EditorCommon.IModel, changeAccessor:EditorCommon.IModelDecorationsChangeAccessor) {
this.decorationIds = [];
this.update(range, model, changeAccessor);
}
public get isCollapsed(): boolean {
return this._isCollapsed;
}
public setCollapsed(isCollaped: boolean, changeAccessor:EditorCommon.IModelDecorationsChangeAccessor): void {
this._isCollapsed = isCollaped;
if (this.decorationIds.length > 0) {
changeAccessor.changeDecorationOptions(this.decorationIds[0], this.getVisualDecorationOptions());
}
}
public getDecorationRange(model:EditorCommon.IModel): EditorCommon.IEditorRange {
if (this.decorationIds.length > 0) {
return model.getDecorationRange(this.decorationIds[1]);
}
return null;
}
private getVisualDecorationOptions(): EditorCommon.IModelDecorationOptions {
if (this._isCollapsed) {
return {
stickiness: EditorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
inlineClassName: 'inline-folded',
linesDecorationsClassName: 'folding collapsed'
};
} else {
return {
stickiness: EditorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
linesDecorationsClassName: 'folding'
};
}
}
private getRangeDecorationOptions(): EditorCommon.IModelDecorationOptions {
return {
stickiness: EditorCommon.TrackedRangeStickiness.GrowsOnlyWhenTypingBefore
}
}
public update(newRange:IFoldingRange, model:EditorCommon.IModel, changeAccessor:EditorCommon.IModelDecorationsChangeAccessor): void {
this._lastRange = newRange;
this._isCollapsed = !!newRange.isCollapsed;
let newDecorations : EditorCommon.IModelDeltaDecoration[] = [];
let maxColumn = model.getLineMaxColumn(newRange.startLineNumber);
let visualRng = {
startLineNumber: newRange.startLineNumber,
startColumn: maxColumn - 1,
endLineNumber: newRange.startLineNumber,
endColumn: maxColumn
};
newDecorations.push({ range: visualRng, options: this.getVisualDecorationOptions() });
let colRng = {
startLineNumber: newRange.startLineNumber,
startColumn: 1,
endLineNumber: newRange.endLineNumber,
endColumn: model.getLineMaxColumn(newRange.endLineNumber)
};
newDecorations.push({ range: colRng, options: this.getRangeDecorationOptions() });
this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, newDecorations);
}
public dispose(changeAccessor:EditorCommon.IModelDecorationsChangeAccessor): void {
this._lastRange = null;
this.decorationIds = changeAccessor.deltaDecorations(this.decorationIds, []);
}
public toString(): string {
let str = this.isCollapsed ? 'collapsed ': 'expanded ';
if (this._lastRange) {
str += (this._lastRange.startLineNumber + '/' + this._lastRange.endLineNumber);
} else {
str += 'no range';
}
return str;
}
}
export class FoldingController implements EditorCommon.IEditorContribution {
static ID = 'editor.contrib.folding';
static getFoldingController(editor:EditorCommon.ICommonCodeEditor): FoldingController {
return <FoldingController>editor.getContribution(FoldingController.ID);
}
private editor: ICodeEditor;
private globalToDispose: IDisposable[];
private computeToken: number;
private updateScheduler: RunOnceScheduler;
private localToDispose: IDisposable[];
private decorations: CollapsibleRegion[];
constructor(editor:ICodeEditor, @INullService nullService) {
this.editor = editor;
this.globalToDispose = [];
this.localToDispose = [];
this.decorations = [];
this.computeToken = 0;
this.globalToDispose.push(this.editor.addListener2(EditorCommon.EventType.ModelChanged, () => this.onModelChanged()));
this.globalToDispose.push(this.editor.addListener2(EditorCommon.EventType.ModelModeChanged, () => this.onModelChanged()));
this.globalToDispose.push(this.editor.addListener2(EditorCommon.EventType.ConfigurationChanged, (e: EditorCommon.IConfigurationChangedEvent) => {
if (e.folding) {
this.onModelChanged();
}
}));
this.onModelChanged();
}
public getId(): string {
return FoldingController.ID;
}
public dispose(): void {
this.cleanState();
this.globalToDispose = disposeAll(this.globalToDispose);
}
/**
* Store view state.
*/
public saveViewState(): any {
let model = this.editor.getModel();
if (!model) {
return {};
}
var collapsedRegions : IFoldingRange[] = [];
this.decorations.forEach(d => {
if (d.isCollapsed) {
var range = d.getDecorationRange(model);
if (range) {
collapsedRegions.push({ startLineNumber: range.startLineNumber, endLineNumber: range.endLineNumber, isCollapsed: true});
}
};
});
return collapsedRegions;
}
/**
* Restore view state.
*/
public restoreViewState(state: any): void {
if (!Array.isArray(state)) {
return;
}
this.applyRegions(<IFoldingRange[]> state);
}
private cleanState(): void {
this.localToDispose = disposeAll(this.localToDispose);
}
private applyRegions(regions: IFoldingRange[]) {
let model = this.editor.getModel();
if (!model) {
return;
}
regions = regions.sort((r1, r2) => r1.startLineNumber - r2.startLineNumber);
log('imput ranges ' + regions.map(rangeToString).join(', '));
this.editor.changeDecorations(changeAccessor => {
let newDecorations : CollapsibleRegion[] = [];
let k = 0, i = 0;
while (i < this.decorations.length && k < regions.length) {
let dec = this.decorations[i];
let decRange = dec.getDecorationRange(model);
if (!decRange) {
log('range no longer valid, was ' + dec.toString());
dec.dispose(changeAccessor);
i++;
} else {
while (k < regions.length && decRange.startLineNumber > regions[k].startLineNumber) {
log('new range ' + rangeToString(regions[k]));
newDecorations.push(new CollapsibleRegion(regions[k], model, changeAccessor));
k++;
}
if (k < regions.length) {
let currRange = regions[k];
if (decRange.startLineNumber < currRange.startLineNumber) {
log('range no longer valid, was ' + dec.toString());
dec.dispose(changeAccessor);
i++;
} else if (decRange.startLineNumber === currRange.startLineNumber) {
currRange.isCollapsed = dec.isCollapsed; // preserve collapse state
dec.update(currRange, model, changeAccessor);
newDecorations.push(dec);
i++;
k++;
}
}
}
}
while (i < this.decorations.length) {
log('range no longer valid, was ' + this.decorations[i].toString());
this.decorations[i].dispose(changeAccessor);
i++;
}
while (k < regions.length) {
log('new range ' + rangeToString(regions[k]));
newDecorations.push(new CollapsibleRegion(regions[k], model, changeAccessor));
k++;
}
this.decorations = newDecorations;
});
this.updateHiddenAreas();
}
private onModelChanged(): void {
this.cleanState();
let model = this.editor.getModel();
if (!this.editor.getConfiguration().folding || !model) {
return;
}
this.updateScheduler = new RunOnceScheduler(() => {
let myToken = (++this.computeToken);
this.computeCollapsibleRegions().then(regions => {
if (myToken !== this.computeToken) {
return; // A new request was made in the meantime or the model was changed
}
this.applyRegions(regions);
});
}, 200);
this.localToDispose.push(this.updateScheduler);
this.localToDispose.push(this.editor.addListener2('change', () => this.updateScheduler.schedule()));
this.localToDispose.push({ dispose: () => {
++this.computeToken;
this.editor.changeDecorations(changeAccessor => {
this.decorations.forEach(dec => dec.dispose(changeAccessor));
this.decorations = [];
});
}});
this.localToDispose.push(this.editor.addListener2(EditorCommon.EventType.MouseDown, (e) => this._onEditorMouseDown(e)));
this.updateScheduler.schedule();
}
private computeCollapsibleRegions(): TPromise<IFoldingRange[]> {
let tabSize = this.editor.getIndentationOptions().tabSize;
let model = this.editor.getModel();
if (!model) {
return TPromise.as([]);
}
let ranges = foldStrategy.computeRanges(model, tabSize);
return TPromise.as(ranges);
}
private _onEditorMouseDown(e: IMouseEvent): void {
if (this.decorations.length === 0) {
return;
}
let range = e.target.range;
if (!range || !range.isEmpty) {
return;
}
if (!e.event.leftButton) {
return;
}
let model = this.editor.getModel();
let toggleClicked = false;
switch (e.target.type) {
case EditorCommon.MouseTargetType.GUTTER_LINE_DECORATIONS:
toggleClicked = true;
break;
case EditorCommon.MouseTargetType.CONTENT_TEXT:
if (range.isEmpty && range.startColumn === model.getLineMaxColumn(range.startLineNumber)) {
break;
}
return;
default:
return;
}
let hasChanges = false;
this.editor.changeDecorations(changeAccessor => {
for (let i = 0; i < this.decorations.length; i++) {
let dec = this.decorations[i];
let decRange = dec.getDecorationRange(model);
if (decRange.startLineNumber === range.startLineNumber) {
if (toggleClicked || dec.isCollapsed) {
dec.setCollapsed(!dec.isCollapsed, changeAccessor);
hasChanges = true;
}
break;
}
}
});
if (hasChanges) {
this.updateHiddenAreas();
}
}
private updateHiddenAreas(): void {
let model = this.editor.getModel();
let hiddenAreas: EditorCommon.IRange[] = [];
this.decorations.filter(dec => dec.isCollapsed).forEach(dec => {
let decRange = dec.getDecorationRange(model);
hiddenAreas.push({
startLineNumber: decRange.startLineNumber + 1,
startColumn: 1,
endLineNumber: decRange.endLineNumber,
endColumn: 1
});
});
this.editor.setHiddenAreas(hiddenAreas);
}
private findRegions(lineNumber: number, collapsed: boolean): CollapsibleRegion[] {
let model = this.editor.getModel();
return this.decorations.filter(dec => {
if (dec.isCollapsed !== collapsed) {
return false;
}
let decRange = dec.getDecorationRange(model);
return decRange && decRange.startLineNumber <= lineNumber && lineNumber < decRange.endLineNumber;
});
}
public unfold(lineNumber: number): void {
let surrounding = this.findRegions(lineNumber, true);
if (surrounding.length > 0) {
this.editor.changeDecorations(changeAccessor => {
surrounding[0].setCollapsed(false, changeAccessor);
});
this.updateHiddenAreas();
}
}
public fold(lineNumber: number): void {
let surrounding = this.findRegions(lineNumber, false);
if (surrounding.length > 0) {
this.editor.changeDecorations(changeAccessor => {
surrounding[surrounding.length - 1].setCollapsed(true, changeAccessor);
});
this.updateHiddenAreas();
}
}
public changeAll(collapse: boolean): void {
if (this.decorations.length > 0) {
let hasChanges = true;
this.editor.changeDecorations(changeAccessor => {
this.decorations.forEach(d => {
if (collapse !== d.isCollapsed) {
d.setCollapsed(collapse, changeAccessor);
hasChanges = true;
}
});
});
if (hasChanges) {
this.updateHiddenAreas();
}
}
}
}
abstract class FoldingAction extends EditorAction {
constructor(descriptor: EditorCommon.IEditorActionDescriptorData, editor: EditorCommon.ICommonCodeEditor, @INullService ns) {
super(descriptor, editor);
}
abstract invoke(foldingController: FoldingController, lineNumber: number): void;
public run(): TPromise<boolean> {
let foldingController = FoldingController.getFoldingController(this.editor);
let selection = this.editor.getSelection();
if (selection && selection.isEmpty) {
this.invoke(foldingController, selection.startLineNumber);
}
return TPromise.as(true);
}
}
class UnfoldAction extends FoldingAction {
public static ID = 'editor.fold';
invoke(foldingController: FoldingController, lineNumber: number): void {
foldingController.unfold(lineNumber);
}
}
class FoldAction extends FoldingAction {
public static ID = 'editor.unfold';
invoke(foldingController: FoldingController, lineNumber: number): void {
foldingController.fold(lineNumber);
}
}
class FoldAllAction extends FoldingAction {
public static ID = 'editor.foldAll';
invoke(foldingController: FoldingController, lineNumber: number): void {
foldingController.changeAll(true);
}
}
class UnfoldAllAction extends FoldingAction {
public static ID = 'editor.unfoldAll';
invoke(foldingController: FoldingController, lineNumber: number): void {
foldingController.changeAll(false);
}
}
EditorBrowserRegistry.registerEditorContribution(FoldingController);
CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(UnfoldAction, UnfoldAction.ID, nls.localize('unfoldAction.label', "Unfold"), {
context: ContextKey.EditorFocus,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET
}));
CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(FoldAction, FoldAction.ID, nls.localize('foldAction.label', "Fold"), {
context: ContextKey.EditorFocus,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET
}));
CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(UnfoldAllAction, UnfoldAllAction.ID, nls.localize('foldAllAction.label', "Fold All"), {
context: ContextKey.EditorFocus,
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.US_OPEN_SQUARE_BRACKET
}));
CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(FoldAllAction, FoldAllAction.ID, nls.localize('unfoldAllAction.label', "Unfold All"), {
context: ContextKey.EditorFocus,
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET
}));
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
export interface IFoldingRange {
startLineNumber:number;
endLineNumber:number;
isCollapsed?:boolean;
}
export function toString(range: IFoldingRange): string {
return (range ? range.startLineNumber + '/' + range.endLineNumber : 'null') + range.isCollapsed ? ' (collapsed)' : '';
}
\ No newline at end of file
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import {TPromise} from 'vs/base/common/winjs.base';
import EditorCommon = require('vs/editor/common/editorCommon');
import {IFoldingRange} from 'vs/editor/contrib/folding/common/foldingRange';
export function computeRanges(model: EditorCommon.IModel, tabSize: number, minimumRangeSize: number = 1): IFoldingRange[] {
let result: IFoldingRange[] = [];
let previousRegions: { indent: number, line: number }[] = [];
previousRegions.push({ indent: -1, line: model.getLineCount() + 1 }); // sentinel, to make sure there's at least one entry
for (let line = model.getLineCount(); line > 0; line--) {
let indent = computeIndentLevel(model.getLineContent(line), tabSize);
if (indent === -1) {
continue; // only whitespace
}
let previous = previousRegions[previousRegions.length - 1];
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.line - 1;
if (endLineNumber - line >= minimumRangeSize) {
result.push({ startLineNumber: line, endLineNumber });
}
}
if (previous.indent === indent) {
previous.line = line;
} else { // previous.indent < indent
// new region with a bigger indent
previousRegions.push({ indent, line });
}
}
return result;
}
function computeIndentLevel(line: string, tabSize: number): number {
let i = 0;
let indent = 0;
while (i < line.length) {
let ch = line.charAt(i);
if (ch === ' ') {
indent++;
} else if (ch === '\t') {
indent++;
indent += (indent % tabSize);
} else {
break;
}
i++;
}
if (i === line.length) {
return -1; // line only consists of whitespace
}
return indent;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import assert = require('assert');
import foldStrategy = require('vs/editor/contrib/folding/common/indentFoldStrategy');
import {IFoldingRange} from 'vs/editor/contrib/folding/common/foldingRange';
import {Model} from 'vs/editor/common/model/model';
suite('Folding', () => {
function assertRanges(lines: string[], tabSize: number, expected:IFoldingRange[]): void {
let model = new Model(lines.join('\n'), null);
let actual = foldStrategy.computeRanges(model, tabSize);
actual.sort((r1, r2) => r1.startLineNumber - r2.startLineNumber);
assert.deepEqual(actual, expected);
model.dispose();
}
function r(startLineNumber: number, endLineNumber: number): IFoldingRange {
return { startLineNumber, endLineNumber };
}
test('t1', () => {
assertRanges([
'A',
' A',
' A',
' A'
], 4, [r(1, 4)]);
});
test('t2', () => {
assertRanges([
'A',
' A',
' A',
' A',
' A'
], 4, [r(1, 5), r(3, 5)] );
});
test('t3', () => {
assertRanges([
'A',
' A',
' A',
' A',
'A'
], 4, [r(1, 4), r(2, 4), r(3, 4)] );
});
test('t4', () => {
assertRanges([
' A',
' A',
'A'
], 4, [] );
});
test('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*/ '}',
], 4, [r(1, 9), r(2, 4), r(7, 8), r(11, 12)] );
});
test('Javadoc', () => {
assertRanges([
/* 1*/ '/**',
/* 2*/ ' * Comment',
/* 3*/ ' */',
/* 4*/ 'class A {',
/* 5*/ ' void foo() {',
/* 6*/ ' }',
/* 7*/ '}',
], 4, [r(1, 3), r(4, 6)] );
});
test('Whitespace', () => {
assertRanges([
/* 1*/ 'class A {',
/* 2*/ '',
/* 3*/ ' void foo() {',
/* 4*/ ' ',
/* 5*/ ' return 0;',
/* 6*/ ' }',
/* 7*/ ' ',
/* 8*/ '}',
], 4, [r(1, 7), r(3, 5)] );
});
})
\ No newline at end of file
......@@ -32,5 +32,5 @@ class SelectBracketAction extends EditorAction {
// register actions
CommonEditorRegistry.registerEditorAction(new EditorActionDescriptor(SelectBracketAction, SelectBracketAction.ID, nls.localize('smartSelect.jumpBracket', "Go to Bracket"), {
context: ContextKey.EditorTextFocus,
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_CLOSE_SQUARE_BRACKET
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.US_BACKSLASH
}));
......@@ -83,8 +83,8 @@ function pos(lineNumber: number, column: number): Position.Position {
return new Position.Position(lineNumber, column);
}
function createSplitLine(splitLengths:number[], wrappedLinesPrefix:string): SplitLinesCollection.SplitLine {
return new SplitLinesCollection.SplitLine(createLineMapping(splitLengths, wrappedLinesPrefix));
function createSplitLine(splitLengths:number[], wrappedLinesPrefix:string, isVisible: boolean = true): SplitLinesCollection.SplitLine {
return new SplitLinesCollection.SplitLine(createLineMapping(splitLengths, wrappedLinesPrefix), isVisible);
}
function createLineMapping(breakingLengths:number[], wrappedLinesPrefix:string): SplitLinesCollection.ILineMapping {
......
......@@ -49,24 +49,20 @@
/* Git dirty diff editor decorations */
.monaco-editor .git-dirty-modified-diff-glyph {
background-color: rgba(0, 122, 204, 0.6);
border-left: 3px solid rgba(0, 122, 204, 0.6);
margin-left: 5px;
width: 3px !important;
}
.monaco-editor.vs-dark .git-dirty-modified-diff-glyph {
background-color: rgba(0, 188, 242, 0.6);
border-left: 3px solid rgba(0, 188, 242, 0.6);
margin-left: 5px;
width: 3px !important;
}
.monaco-editor .git-dirty-added-diff-glyph {
background-color: rgba(45, 136, 62, 0.6);
border-left: 3px solid rgba(45, 136, 62, 0.6);
margin-left: 5px;
width: 3px !important;
}
.monaco-editor.vs-dark .git-dirty-added-diff-glyph {
background-color: rgba(127, 186, 0, 0.6);
border-left: 3px solid rgba(127, 186, 0, 0.6);
margin-left: 5px;
width: 3px !important;
}
.monaco-editor .git-dirty-deleted-diff-glyph:after {
content: '';
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册