diff --git a/src/vs/editor/browser/editor.all.js b/src/vs/editor/browser/editor.all.js index 01b2e0469ae1ad9367f9d940c1268456a7d94c55..7d90a1c03bfadca79ab73f2f77bdac516866472b 100644 --- a/src/vs/editor/browser/editor.all.js +++ b/src/vs/editor/browser/editor.all.js @@ -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' diff --git a/src/vs/editor/browser/editorBrowser.ts b/src/vs/editor/browser/editorBrowser.ts index 0e4167f8a3038b94e8d15bdd4757c70cddbac287..a91f7cd7334bcd66b78487873568e8d3b41ce25b 100644 --- a/src/vs/editor/browser/editorBrowser.ts +++ b/src/vs/editor/browser/editorBrowser.ts @@ -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; } /** diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 35826c8583e1ada69c078ad2d436d30006b10771..c98641147fcc805a5bb4f8240c9af4d484d1b5a5 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -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([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; diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 0ac5e627141f7722820353011bdb43a8ae90455a..99a8ae8ae48d62ca9938b1f916ea1b81ab1b43a2 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -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, diff --git a/src/vs/editor/common/config/defaultConfig.ts b/src/vs/editor/common/config/defaultConfig.ts index 8d34c98b5bd7c45149e57eb58e798ddf8e01cebb..cfdbbae69d556f2b521d5a4c937f14f4b32171e3 100644 --- a/src/vs/editor/common/config/defaultConfig.ts +++ b/src/vs/editor/common/config/defaultConfig.ts @@ -67,6 +67,7 @@ class ConfigClass implements IConfiguration { selectionHighlight: true, outlineMarkers: false, referenceInfos: true, + folding: true, renderWhitespace: false, tabSize: 4, diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 83039a5e2927365bc62fa8f5fbd3eb769103bd4c..0fd266f8d6eafe17157e178876f54b68255c0afa 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -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 }; diff --git a/src/vs/editor/common/viewModel/splitLinesCollection.ts b/src/vs/editor/common/viewModel/splitLinesCollection.ts index ac8f0dc831bd8a12b22e09e940b174acf4c5a8ed..cecb9a5bc6454090a6b643e08e5e33ef72b6aaf8 100644 --- a/src/vs/editor/common/viewModel/splitLinesCollection.ts +++ b/src/vs/editor/common/viewModel/splitLinesCollection.ts @@ -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; } diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index c35177227a1258fda17dc295a18e6e6b5e0f9fcd..57c05fe225a837068bc7a2c6f153c7ee3248961d 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -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; diff --git a/src/vs/editor/contrib/folding/browser/arrow-collapse-dark.svg b/src/vs/editor/contrib/folding/browser/arrow-collapse-dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..233310dca9532f0e02a576a427acb65c22efa8d9 --- /dev/null +++ b/src/vs/editor/contrib/folding/browser/arrow-collapse-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/folding/browser/arrow-collapse.svg b/src/vs/editor/contrib/folding/browser/arrow-collapse.svg new file mode 100644 index 0000000000000000000000000000000000000000..5dcb87c772c21d11d48719811309038c2a47f565 --- /dev/null +++ b/src/vs/editor/contrib/folding/browser/arrow-collapse.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/folding/browser/arrow-expand-dark.svg b/src/vs/editor/contrib/folding/browser/arrow-expand-dark.svg new file mode 100644 index 0000000000000000000000000000000000000000..46550b419e45ef90484b79119f4853ca53f575da --- /dev/null +++ b/src/vs/editor/contrib/folding/browser/arrow-expand-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/folding/browser/arrow-expand.svg b/src/vs/editor/contrib/folding/browser/arrow-expand.svg new file mode 100644 index 0000000000000000000000000000000000000000..e55ccd923e52bc53c2c69eb85b1bd1cdf0ea77ed --- /dev/null +++ b/src/vs/editor/contrib/folding/browser/arrow-expand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/vs/editor/contrib/folding/browser/folding.css b/src/vs/editor/contrib/folding/browser/folding.css new file mode 100644 index 0000000000000000000000000000000000000000..dec103e82275ffb28f0638f9168de13d1c5fda95 --- /dev/null +++ b/src/vs/editor/contrib/folding/browser/folding.css @@ -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. + *--------------------------------------------------------------------------------------------*/ + +.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; +} + diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts new file mode 100644 index 0000000000000000000000000000000000000000..b9e719c74d377fabfa4f87b99cad22f3b2a35824 --- /dev/null +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -0,0 +1,490 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {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 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( 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 { + 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 { + 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 diff --git a/src/vs/editor/contrib/folding/common/foldingRange.ts b/src/vs/editor/contrib/folding/common/foldingRange.ts new file mode 100644 index 0000000000000000000000000000000000000000..30d57d17b921dea5e5c9cb53321d5d4f6bf7c29a --- /dev/null +++ b/src/vs/editor/contrib/folding/common/foldingRange.ts @@ -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. + *--------------------------------------------------------------------------------------------*/ + +'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 diff --git a/src/vs/editor/contrib/folding/common/indentFoldStrategy.ts b/src/vs/editor/contrib/folding/common/indentFoldStrategy.ts new file mode 100644 index 0000000000000000000000000000000000000000..18c6dcf9bce3a58bc19230ee02e310a948bdd735 --- /dev/null +++ b/src/vs/editor/contrib/folding/common/indentFoldStrategy.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * 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; +} diff --git a/src/vs/editor/contrib/folding/test/indentFold.test.ts b/src/vs/editor/contrib/folding/test/indentFold.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..5344e1e87ef81019234ab08f4ed414d566565883 --- /dev/null +++ b/src/vs/editor/contrib/folding/test/indentFold.test.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * 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 diff --git a/src/vs/editor/contrib/smartSelect/common/jumpToBracket.ts b/src/vs/editor/contrib/smartSelect/common/jumpToBracket.ts index 6430419a9b54a3daf5ef92dab7ad5b00fc8bdc0f..924e28a65702207f30a1a185b1df2871f814e246 100644 --- a/src/vs/editor/contrib/smartSelect/common/jumpToBracket.ts +++ b/src/vs/editor/contrib/smartSelect/common/jumpToBracket.ts @@ -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 })); diff --git a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts index 709d070b60ebbe5f4255ce945ae3a8879129f18a..881cbdefe7e8bf2f4d88f3d74174b6d2db08bf99 100644 --- a/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts +++ b/src/vs/editor/test/common/viewModel/splitLinesCollection.test.ts @@ -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 { diff --git a/src/vs/workbench/parts/git/browser/media/git.contribution.css b/src/vs/workbench/parts/git/browser/media/git.contribution.css index 88ce4267cb4b7e0ff096563a1196b6ea73138867..2ef8b90efb3f1b01da1f395e2b5724731dbbd6ee 100644 --- a/src/vs/workbench/parts/git/browser/media/git.contribution.css +++ b/src/vs/workbench/parts/git/browser/media/git.contribution.css @@ -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: '';