diff --git a/extensions/clojure/language-configuration.json b/extensions/clojure/language-configuration.json index 004e1dbd95b..b68c24d92e3 100644 --- a/extensions/clojure/language-configuration.json +++ b/extensions/clojure/language-configuration.json @@ -18,5 +18,10 @@ ["[", "]"], ["(", ")"], ["\"", "\""] - ] + ], + "folding": { + "indendationBasedFolding": { + "offSide": true + } + } } \ No newline at end of file diff --git a/extensions/coffeescript/language-configuration.json b/extensions/coffeescript/language-configuration.json index 745427eacfb..be41b444061 100644 --- a/extensions/coffeescript/language-configuration.json +++ b/extensions/coffeescript/language-configuration.json @@ -21,5 +21,10 @@ ["(", ")"], ["\"", "\""], ["'", "'"] - ] + ], + "folding": { + "indendationBasedFolding": { + "offSide": true + } + } } \ No newline at end of file diff --git a/extensions/fsharp/language-configuration.json b/extensions/fsharp/language-configuration.json index 89dc2adb5a8..8a88df12433 100644 --- a/extensions/fsharp/language-configuration.json +++ b/extensions/fsharp/language-configuration.json @@ -20,5 +20,10 @@ ["(", ")"], ["\"", "\""], ["'", "'"] - ] + ], + "folding": { + "indendationBasedFolding": { + "offSide": true + } + } } diff --git a/extensions/git/src/git.ts b/extensions/git/src/git.ts index 3cd0dadfdac..6600d727131 100644 --- a/extensions/git/src/git.ts +++ b/extensions/git/src/git.ts @@ -66,7 +66,7 @@ function findSpecificGit(path: string): Promise { const buffers: Buffer[] = []; const child = cp.spawn(path, ['--version']); child.stdout.on('data', (b: Buffer) => buffers.push(b)); - child.on('error', e); + child.on('error', cpErrorHandler(e)); child.on('exit', code => code ? e(new Error('Not found')) : c({ path, version: parseVersion(Buffer.concat(buffers).toString('utf8').trim()) })); }); } @@ -159,6 +159,20 @@ export interface IExecutionResult { stderr: string; } +function cpErrorHandler(cb: (reason?: any) => void): (reason?: any) => void { + return err => { + if (/ENOENT/.test(err.message)) { + err = new GitError({ + error: err, + message: 'Failed to execute git (ENOENT)', + gitErrorCode: GitErrorCodes.NotAGitRepository + }); + } + + cb(err); + }; +} + async function exec(child: cp.ChildProcess, options: any = {}): Promise { if (!child.stdout || !child.stderr) { throw new GitError({ @@ -183,7 +197,7 @@ async function exec(child: cp.ChildProcess, options: any = {}): Promise([ new Promise((c, e) => { - once(child, 'error', e); + once(child, 'error', cpErrorHandler(e)); once(child, 'exit', c); }), new Promise(c => { @@ -919,7 +933,7 @@ export class Repository { child.stderr.setEncoding('utf8'); child.stderr.on('data', raw => stderrData.push(raw as string)); - child.on('error', e); + child.on('error', cpErrorHandler(e)); child.on('exit', onExit); }); } diff --git a/extensions/markdown/language-configuration.json b/extensions/markdown/language-configuration.json index 6c811c66aa6..c328f05d0e0 100644 --- a/extensions/markdown/language-configuration.json +++ b/extensions/markdown/language-configuration.json @@ -37,5 +37,10 @@ ["(", ")"], ["[", "]"], ["`", "`"] - ] + ], + "folding": { + "indendationBasedFolding": { + "offSide": true + } + } } \ No newline at end of file diff --git a/extensions/pug/language-configuration.json b/extensions/pug/language-configuration.json index e22b6120252..96f950546e6 100644 --- a/extensions/pug/language-configuration.json +++ b/extensions/pug/language-configuration.json @@ -20,5 +20,10 @@ ["(", ")"], ["'", "'"], ["\"", "\""] - ] + ], + "folding": { + "indendationBasedFolding": { + "offSide": true + } + } } \ No newline at end of file diff --git a/extensions/python/language-configuration.json b/extensions/python/language-configuration.json index c995ea91f3d..7d9f13e6ce5 100644 --- a/extensions/python/language-configuration.json +++ b/extensions/python/language-configuration.json @@ -21,6 +21,10 @@ ["(", ")"], ["\"", "\""], ["'", "'"] - ] - // enhancedBrackets: [ { open: /.*:\s*$/, closeComplete: 'else:' } ], + ], + "folding": { + "indendationBasedFolding": { + "offSide": true + } + } } \ No newline at end of file diff --git a/extensions/yaml/language-configuration.json b/extensions/yaml/language-configuration.json index cab4f6602ff..3d122eed6e0 100644 --- a/extensions/yaml/language-configuration.json +++ b/extensions/yaml/language-configuration.json @@ -20,5 +20,10 @@ ["(", ")"], ["\"", "\""], ["'", "'"] - ] + ], + "folding": { + "indendationBasedFolding": { + "offSide": true + } + } } \ No newline at end of file diff --git a/src/vs/code/electron-main/windows.ts b/src/vs/code/electron-main/windows.ts index d29017c2752..967b5d51b66 100644 --- a/src/vs/code/electron-main/windows.ts +++ b/src/vs/code/electron-main/windows.ts @@ -1820,7 +1820,7 @@ class WorkspacesManager { } else { const resolvedWorkspace = this.workspacesService.resolveWorkspaceSync(workspace.configPath); if (resolvedWorkspace && resolvedWorkspace.folders.length > 0) { - defaultPath = dirname(resolvedWorkspace.folders[0].path); + defaultPath = dirname(resolvedWorkspace.folders[0].uri.fsPath); } } } diff --git a/src/vs/code/node/windowsFinder.ts b/src/vs/code/node/windowsFinder.ts index d8b0d1e93d4..6d17ad333a3 100644 --- a/src/vs/code/node/windowsFinder.ts +++ b/src/vs/code/node/windowsFinder.ts @@ -62,7 +62,7 @@ function findWindowOnFilePath(windows: W[], filePath: s for (let i = 0; i < workspaceWindows.length; i++) { const window = workspaceWindows[i]; const resolvedWorkspace = workspaceResolver(window.openedWorkspace); - if (resolvedWorkspace && resolvedWorkspace.folders.some(folder => paths.isEqualOrParent(filePath, folder.path, !platform.isLinux /* ignorecase */))) { + if (resolvedWorkspace && resolvedWorkspace.folders.some(folder => paths.isEqualOrParent(filePath, folder.uri.fsPath, !platform.isLinux /* ignorecase */))) { return window; } } diff --git a/src/vs/code/test/node/windowsFinder.test.ts b/src/vs/code/test/node/windowsFinder.test.ts index 0651a98d6b2..2c0b84336ff 100644 --- a/src/vs/code/test/node/windowsFinder.test.ts +++ b/src/vs/code/test/node/windowsFinder.test.ts @@ -9,6 +9,7 @@ import path = require('path'); import { findBestWindowOrFolderForFile, ISimpleWindow, IBestWindowOrFolderOptions } from 'vs/code/node/windowsFinder'; import { OpenContext } from 'vs/platform/windows/common/windows'; import { IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; const fixturesFolder = require.toUrl('./fixtures'); @@ -24,7 +25,7 @@ function options(custom?: Partial>): I reuseWindow: false, context: OpenContext.CLI, codeSettingsFolder: '_vscode', - workspaceResolver: workspace => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: [{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }] } : null; }, + workspaceResolver: workspace => { return workspace === testWorkspace ? { id: testWorkspace.id, configPath: workspace.configPath, folders: toWorkspaceFolders([{ path: path.join(fixturesFolder, 'vscode_workspace_1_folder') }, { path: path.join(fixturesFolder, 'vscode_workspace_2_folder') }]) } : null; }, ...custom }; } diff --git a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts index dfd4f11d014..b50adb24b77 100644 --- a/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts +++ b/src/vs/editor/browser/viewParts/indentGuides/indentGuides.ts @@ -77,6 +77,9 @@ export class IndentGuidesOverlay extends DynamicViewOverlay { public onZonesChanged(e: viewEvents.ViewZonesChangedEvent): boolean { return true; } + public onLanguageConfigurationChanged(e: viewEvents.ViewLanguageConfigurationEvent): boolean { + return true; + } // --- end event handlers diff --git a/src/vs/editor/common/commonCodeEditor.ts b/src/vs/editor/common/commonCodeEditor.ts index 29700d8baf2..e3f5394ff42 100644 --- a/src/vs/editor/common/commonCodeEditor.ts +++ b/src/vs/editor/common/commonCodeEditor.ts @@ -23,7 +23,7 @@ import { hash } from 'vs/base/common/hash'; import { EditorModeContext } from 'vs/editor/common/modes/editorModeContext'; import { IModelContentChangedEvent, IModelDecorationsChangedEvent, - IModelLanguageChangedEvent, IModelOptionsChangedEvent, TextModelEventType + IModelLanguageChangedEvent, IModelOptionsChangedEvent, TextModelEventType, IModelLanguageConfigurationChangedEvent } from 'vs/editor/common/model/textModelEvents'; import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; @@ -44,6 +44,9 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo private readonly _onDidChangeModelLanguage: Emitter = this._register(new Emitter()); public readonly onDidChangeModelLanguage: Event = this._onDidChangeModelLanguage.event; + private readonly _onDidChangeModelLanguageConfiguration: Emitter = this._register(new Emitter()); + public readonly onDidChangeModelLanguageConfiguration: Event = this._onDidChangeModelLanguage.event; + private readonly _onDidChangeModelOptions: Emitter = this._register(new Emitter()); public readonly onDidChangeModelOptions: Event = this._onDidChangeModelOptions.event; @@ -170,7 +173,7 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo // editor actions don't need to be disposed this._actions = {}; - + this._removeDecorationTypes(); this._postDetachModelCleanup(this._detachModel()); this._onDidDispose.fire(); @@ -232,10 +235,24 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo newModelUrl: model ? model.uri : null }; + this._removeDecorationTypes(); this._onDidChangeModel.fire(e); this._postDetachModelCleanup(detachedModel); } + private _removeDecorationTypes(): void { + this._decorationTypeKeysToIds = {}; + if (this._decorationTypeSubtypes) { + for (let decorationType in this._decorationTypeSubtypes) { + let subTypes = this._decorationTypeSubtypes[decorationType]; + for (let subType in subTypes) { + this._removeDecorationType(decorationType + '-' + subType); + } + } + this._decorationTypeSubtypes = {}; + } + } + public getCenteredRangeInViewport(): Range { if (!this.hasView) { return null; @@ -887,6 +904,10 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo this._onDidChangeModelLanguage.fire(e); break; + case TextModelEventType.ModelLanguageConfigurationChanged: + this._onDidChangeModelLanguageConfiguration.fire(e); + break; + case TextModelEventType.ModelContentChanged: this._onDidChangeModelContent.fire(e); break; @@ -948,16 +969,6 @@ export abstract class CommonCodeEditor extends Disposable implements editorCommo protected _postDetachModelCleanup(detachedModel: editorCommon.IModel): void { if (detachedModel) { - this._decorationTypeKeysToIds = {}; - if (this._decorationTypeSubtypes) { - for (let decorationType in this._decorationTypeSubtypes) { - let subTypes = this._decorationTypeSubtypes[decorationType]; - for (let subType in subTypes) { - this._removeDecorationType(decorationType + '-' + subType); - } - } - this._decorationTypeSubtypes = {}; - } detachedModel.removeAllDecorationsWithOwnerId(this.id); } } diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 89edd8d344c..65bc4059e5c 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -13,7 +13,6 @@ import { Range } from 'vs/editor/common/core/range'; import { Selection, SelectionDirection, ISelection } from 'vs/editor/common/core/selection'; import * as editorCommon from 'vs/editor/common/editorCommon'; import { CursorColumns, CursorConfiguration, EditOperationResult, CursorContext, CursorState, RevealTarget, IColumnSelectData, ICursors } from 'vs/editor/common/controller/cursorCommon'; -import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { DeleteOperations } from 'vs/editor/common/controller/cursorDeleteOperations'; import { TypeOperations } from 'vs/editor/common/controller/cursorTypeOperations'; import { TextModelEventType, ModelRawContentChangedEvent, RawContentChangedType } from 'vs/editor/common/model/textModelEvents'; @@ -150,11 +149,10 @@ export class Cursor extends viewEvents.ViewEventEmitter implements ICursors { this.context = new CursorContext(this._configuration, this._model, this._viewModel); this._cursors.updateContext(this.context); }; - this._register(this._model.onDidChangeLanguage((e) => { + this._register(model.onDidChangeLanguage((e) => { updateCursorContext(); })); - this._register(LanguageConfigurationRegistry.onDidChange(() => { - // TODO@Alex: react only if certain supports changed? (and if my model's mode changed) + this._register(model.onDidChangeLanguageConfiguration(() => { updateCursorContext(); })); this._register(model.onDidChangeOptions(() => { diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index a8faeb62a75..ab9db53d0ce 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -19,7 +19,7 @@ import { IndentRange } from 'vs/editor/common/model/indentRanges'; import { ITextSource } from 'vs/editor/common/model/textSource'; import { ModelRawContentChangedEvent, IModelContentChangedEvent, IModelDecorationsChangedEvent, - IModelLanguageChangedEvent, IModelOptionsChangedEvent + IModelLanguageChangedEvent, IModelOptionsChangedEvent, IModelLanguageConfigurationChangedEvent } from 'vs/editor/common/model/textModelEvents'; import * as editorOptions from 'vs/editor/common/config/editorOptions'; import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; @@ -585,26 +585,17 @@ export interface ITextModel { */ getLineContent(lineNumber: number): string; - /** - * @internal - */ - getIndentLevel(lineNumber: number): number; - - /** - * @internal - */ - getIndentRanges(): IndentRange[]; - - /** - * @internal - */ - getLineIndentGuide(lineNumber: number): number; /** * Get the text for all lines. */ getLinesContent(): string[]; + /** + * @internal + */ + getIndentLevel(lineNumber: number): number; + /** * Get the end of line sequence predominantly used in the text buffer. * @return EOL char sequence (e.g.: '\n' or '\r\n'). @@ -912,6 +903,16 @@ export interface ITokenizedModel extends ITextModel { * @internal */ matchBracket(position: IPosition): [Range, Range]; + + /** + * @internal + */ + getIndentRanges(): IndentRange[]; + + /** + * @internal + */ + getLineIndentGuide(lineNumber: number): number; } /** @@ -1149,6 +1150,11 @@ export interface IModel extends IReadOnlyModel, IEditableTextModel, ITextModelWi * @event */ onDidChangeLanguage(listener: (e: IModelLanguageChangedEvent) => void): IDisposable; + /** + * An event emitted when the language configuration associated with the model has changed. + * @event + */ + onDidChangeLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) => void): IDisposable; /** * An event emitted right before disposing the model. * @event @@ -1748,6 +1754,11 @@ export interface ICommonCodeEditor extends IEditor { * @event */ onDidChangeModelLanguage(listener: (e: IModelLanguageChangedEvent) => void): IDisposable; + /** + * An event emitted when the language configuration of the current model has changed. + * @event + */ + onDidChangeModelLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) => void): IDisposable; /** * An event emitted when the options of the current model has changed. * @event diff --git a/src/vs/editor/common/model/indentRanges.ts b/src/vs/editor/common/model/indentRanges.ts index f220df0cfba..a26176a5817 100644 --- a/src/vs/editor/common/model/indentRanges.ts +++ b/src/vs/editor/common/model/indentRanges.ts @@ -29,7 +29,7 @@ export class IndentRange { } } -export function computeRanges(model: ITextModel, minimumRangeSize: number = 1): IndentRange[] { +export function computeRanges(model: ITextModel, offSide: boolean, minimumRangeSize: number = 1): IndentRange[] { let result: IndentRange[] = []; @@ -38,11 +38,15 @@ export function computeRanges(model: ITextModel, minimumRangeSize: number = 1): for (let line = model.getLineCount(); line > 0; line--) { let indent = model.getIndentLevel(line); + let previous = previousRegions[previousRegions.length - 1]; if (indent === -1) { + if (offSide) { + // for offSide languages, empty lines are associated to the next block + previous.line = line; + } continue; // only whitespace } - let previous = previousRegions[previousRegions.length - 1]; if (previous.indent > indent) { // discard all regions with larger indent diff --git a/src/vs/editor/common/model/model.ts b/src/vs/editor/common/model/model.ts index aef76d48016..1228592fe0f 100644 --- a/src/vs/editor/common/model/model.ts +++ b/src/vs/editor/common/model/model.ts @@ -34,7 +34,9 @@ export class Model extends EditableTextModel implements IModel { public onDidChangeLanguage(listener: (e: textModelEvents.IModelLanguageChangedEvent) => void): IDisposable { return this._eventEmitter.addListener(textModelEvents.TextModelEventType.ModelLanguageChanged, listener); } - + public onDidChangeLanguageConfiguration(listener: (e: textModelEvents.IModelLanguageConfigurationChangedEvent) => void): IDisposable { + return this._eventEmitter.addListener(textModelEvents.TextModelEventType.ModelLanguageConfigurationChanged, listener); + } public static createFromString(text: string, options: ITextModelCreationOptions = TextModel.DEFAULT_CREATION_OPTIONS, languageIdentifier: LanguageIdentifier = null, uri: URI = null): Model { return new Model(RawTextSource.fromString(text), options, languageIdentifier, uri); } diff --git a/src/vs/editor/common/model/textModel.ts b/src/vs/editor/common/model/textModel.ts index ce088944f8d..3e55882ed96 100644 --- a/src/vs/editor/common/model/textModel.ts +++ b/src/vs/editor/common/model/textModel.ts @@ -13,7 +13,6 @@ import { ModelLine, IModelLine, MinimalModelLine } from 'vs/editor/common/model/ import { guessIndentation } from 'vs/editor/common/model/indentationGuesser'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { PrefixSumComputer } from 'vs/editor/common/viewModel/prefixSumComputer'; -import { IndentRange, computeRanges } from 'vs/editor/common/model/indentRanges'; import { TextModelSearch, SearchParams } from 'vs/editor/common/model/textModelSearch'; import { TextSource, ITextSource, IRawTextSource, RawTextSource } from 'vs/editor/common/model/textSource'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -85,7 +84,6 @@ export class TextModel implements editorCommon.ITextModel { protected _isDisposing: boolean; protected _options: editorCommon.TextModelResolvedOptions; protected _lineStarts: PrefixSumComputer; - private _indentRanges: IndentRange[]; private _versionId: number; /** @@ -513,65 +511,6 @@ export class TextModel implements editorCommon.ITextModel { return this._lines[lineNumber - 1].getIndentLevel(); } - protected _resetIndentRanges(): void { - this._indentRanges = null; - } - - private _getIndentRanges(): IndentRange[] { - if (!this._indentRanges) { - this._indentRanges = computeRanges(this); - } - return this._indentRanges; - } - - public getIndentRanges(): IndentRange[] { - this._assertNotDisposed(); - let indentRanges = this._getIndentRanges(); - return IndentRange.deepCloneArr(indentRanges); - } - - private _toValidLineIndentGuide(lineNumber: number, indentGuide: number): number { - let lineIndentLevel = this._lines[lineNumber - 1].getIndentLevel(); - if (lineIndentLevel === -1) { - return indentGuide; - } - let maxIndentGuide = Math.ceil(lineIndentLevel / this._options.tabSize); - return Math.min(maxIndentGuide, indentGuide); - } - - public getLineIndentGuide(lineNumber: number): number { - this._assertNotDisposed(); - if (lineNumber < 1 || lineNumber > this.getLineCount()) { - throw new Error('Illegal value ' + lineNumber + ' for `lineNumber`'); - } - - let indentRanges = this._getIndentRanges(); - - for (let i = indentRanges.length - 1; i >= 0; i--) { - let rng = indentRanges[i]; - - if (rng.startLineNumber === lineNumber) { - return this._toValidLineIndentGuide(lineNumber, Math.ceil(rng.indent / this._options.tabSize)); - } - if (rng.startLineNumber < lineNumber && lineNumber <= rng.endLineNumber) { - return this._toValidLineIndentGuide(lineNumber, 1 + Math.floor(rng.indent / this._options.tabSize)); - } - if (rng.endLineNumber + 1 === lineNumber) { - let bestIndent = rng.indent; - while (i > 0) { - i--; - rng = indentRanges[i]; - if (rng.endLineNumber + 1 === lineNumber) { - bestIndent = rng.indent; - } - } - return this._toValidLineIndentGuide(lineNumber, Math.ceil(bestIndent / this._options.tabSize)); - } - } - - return 0; - } - public getLinesContent(): string[] { this._assertNotDisposed(); var r: string[] = []; @@ -785,7 +724,6 @@ export class TextModel implements editorCommon.ITextModel { this._EOL = textSource.EOL; this._lines = modelLines; this._lineStarts = null; - this._resetIndentRanges(); } private _getEndOfLine(eol: editorCommon.EndOfLinePreference): string { diff --git a/src/vs/editor/common/model/textModelEvents.ts b/src/vs/editor/common/model/textModelEvents.ts index 890dd25df46..2acaf260be2 100644 --- a/src/vs/editor/common/model/textModelEvents.ts +++ b/src/vs/editor/common/model/textModelEvents.ts @@ -18,6 +18,7 @@ export const TextModelEventType = { ModelContentChanged: 'contentChanged', ModelRawContentChanged2: 'rawContentChanged2', ModelDecorationsChanged: 'decorationsChanged', + ModelLanguageConfigurationChanged: 'modelLanguageConfigurationChanged' }; /** @@ -34,6 +35,12 @@ export interface IModelLanguageChangedEvent { readonly newLanguage: string; } +/** + * An event describing that the language configuration associated with a model has changed. + */ +export interface IModelLanguageConfigurationChangedEvent { +} + export interface IModelContentChange { /** * The range that got replaced. diff --git a/src/vs/editor/common/model/textModelWithTokens.ts b/src/vs/editor/common/model/textModelWithTokens.ts index d4f31ffcc58..14c19d4a080 100644 --- a/src/vs/editor/common/model/textModelWithTokens.ts +++ b/src/vs/editor/common/model/textModelWithTokens.ts @@ -22,6 +22,7 @@ import { getWordAtText } from 'vs/editor/common/model/wordHelper'; import { TokenizationResult2 } from 'vs/editor/common/core/token'; import { ITextSource, IRawTextSource } from 'vs/editor/common/model/textSource'; import * as textModelEvents from 'vs/editor/common/model/textModelEvents'; +import { IndentRange, computeRanges } from 'vs/editor/common/model/indentRanges'; class ModelTokensChangedEventBuilder { @@ -69,6 +70,9 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke private _invalidLineStartIndex: number; private _lastState: IState; + private _indentRanges: IndentRange[]; + private _languageRegistryListener: IDisposable; + private _revalidateTokensTimeout: number; constructor(rawTextSource: IRawTextSource, creationOptions: editorCommon.ITextModelCreationOptions, languageIdentifier: LanguageIdentifier) { @@ -95,11 +99,20 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke this._revalidateTokensTimeout = -1; + this._languageRegistryListener = LanguageConfigurationRegistry.onDidChange((e) => { + if (e.languageIdentifier.id === this._languageIdentifier.id) { + this._resetIndentRanges(); + this._emitModelLanguageConfigurationEvent({}); + } + }); + this._resetTokenizationState(); + this._resetIndentRanges(); } public dispose(): void { this._tokenizationListener.dispose(); + this._languageRegistryListener.dispose(); this._clearTimers(); this._lastState = null; @@ -114,6 +127,7 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke super._resetValue(newValue); // Cancel tokenization, clear all tokens and begin tokenizing this._resetTokenizationState(); + this._resetIndentRanges(); } protected _resetTokenizationState(): void { @@ -225,6 +239,7 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke // Cancel tokenization, clear all tokens and begin tokenizing this._resetTokenizationState(); + this._resetIndentRanges(); this.emitModelTokensChangedEvent({ ranges: [{ @@ -233,6 +248,7 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke }] }); this._emitModelModeChangedEvent(e); + this._emitModelLanguageConfigurationEvent({}); } public getLanguageIdAtPosition(_lineNumber: number, _column: number): LanguageId { @@ -398,6 +414,12 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke } } + private _emitModelLanguageConfigurationEvent(e: textModelEvents.IModelLanguageConfigurationChangedEvent): void { + if (!this._isDisposing) { + this._eventEmitter.emit(textModelEvents.TextModelEventType.ModelLanguageConfigurationChanged, e); + } + } + private _emitModelModeChangedEvent(e: textModelEvents.IModelLanguageChangedEvent): void { if (!this._isDisposing) { this._eventEmitter.emit(textModelEvents.TextModelEventType.ModelLanguageChanged, e); @@ -814,4 +836,65 @@ export class TextModelWithTokens extends TextModel implements editorCommon.IToke isOpen: modeBrackets.textIsOpenBracket[text] }; } + + protected _resetIndentRanges(): void { + this._indentRanges = null; + } + + private _getIndentRanges(): IndentRange[] { + if (!this._indentRanges) { + let foldingRules = LanguageConfigurationRegistry.getFoldingRules(this._languageIdentifier.id); + let offSide = foldingRules && foldingRules.indendationBasedFolding && foldingRules.indendationBasedFolding.offSide; + this._indentRanges = computeRanges(this, offSide); + } + return this._indentRanges; + } + + public getIndentRanges(): IndentRange[] { + this._assertNotDisposed(); + let indentRanges = this._getIndentRanges(); + return IndentRange.deepCloneArr(indentRanges); + } + + public getLineIndentGuide(lineNumber: number): number { + this._assertNotDisposed(); + if (lineNumber < 1 || lineNumber > this.getLineCount()) { + throw new Error('Illegal value ' + lineNumber + ' for `lineNumber`'); + } + + let indentRanges = this._getIndentRanges(); + + for (let i = indentRanges.length - 1; i >= 0; i--) { + let rng = indentRanges[i]; + + if (rng.startLineNumber === lineNumber) { + return this._toValidLineIndentGuide(lineNumber, Math.ceil(rng.indent / this._options.tabSize)); + } + if (rng.startLineNumber < lineNumber && lineNumber <= rng.endLineNumber) { + return this._toValidLineIndentGuide(lineNumber, 1 + Math.floor(rng.indent / this._options.tabSize)); + } + if (rng.endLineNumber + 1 === lineNumber) { + let bestIndent = rng.indent; + while (i > 0) { + i--; + rng = indentRanges[i]; + if (rng.endLineNumber + 1 === lineNumber) { + bestIndent = rng.indent; + } + } + return this._toValidLineIndentGuide(lineNumber, Math.ceil(bestIndent / this._options.tabSize)); + } + } + + return 0; + } + + private _toValidLineIndentGuide(lineNumber: number, indentGuide: number): number { + let lineIndentLevel = this._lines[lineNumber - 1].getIndentLevel(); + if (lineIndentLevel === -1) { + return indentGuide; + } + let maxIndentGuide = Math.ceil(lineIndentLevel / this._options.tabSize); + return Math.min(maxIndentGuide, indentGuide); + } } diff --git a/src/vs/editor/common/modes/languageConfiguration.ts b/src/vs/editor/common/modes/languageConfiguration.ts index 1fec47f4806..ad764283935 100644 --- a/src/vs/editor/common/modes/languageConfiguration.ts +++ b/src/vs/editor/common/modes/languageConfiguration.ts @@ -61,6 +61,12 @@ export interface LanguageConfiguration { * settings will be used. */ surroundingPairs?: IAutoClosingPair[]; + + /** + * The language's folding rules. + */ + folding?: FoldingRules; + /** * **Deprecated** Do not use. * @@ -89,8 +95,25 @@ export interface IndentationRule { * If a line matches this pattern, then its indentation should not be changed and it should not be evaluated against the other rules. */ unIndentedLinePattern?: RegExp; + } +/** + * Describes folding rules for a language. + */ +export interface FoldingRules { + indendationBasedFolding?: { + /** + * Used by the indentation based strategy to decide wheter empty lines belong to the previous or the next block. + * A language adheres to the off-side rule if blocks in that language are expressed by their indentation. + * See [wikipedia](https://en.wikipedia.org/wiki/Off-side_rule) for more information. + * If not set, `false` is used and empty lines belong to the previous block. + */ + offSide: boolean; + }; +} + + /** * Describes a rule to be evaluated when pressing Enter. */ diff --git a/src/vs/editor/common/modes/languageConfigurationRegistry.ts b/src/vs/editor/common/modes/languageConfigurationRegistry.ts index dd3e1ec8acc..f069921add2 100644 --- a/src/vs/editor/common/modes/languageConfigurationRegistry.ts +++ b/src/vs/editor/common/modes/languageConfigurationRegistry.ts @@ -18,7 +18,7 @@ import { DEFAULT_WORD_REGEXP, ensureValidWordDefinition } from 'vs/editor/common import { createScopedLineTokens } from 'vs/editor/common/modes/supports'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Range } from 'vs/editor/common/core/range'; -import { IndentAction, EnterAction, IAutoClosingPair, LanguageConfiguration, IndentationRule } from 'vs/editor/common/modes/languageConfiguration'; +import { IndentAction, EnterAction, IAutoClosingPair, LanguageConfiguration, IndentationRule, FoldingRules } from 'vs/editor/common/modes/languageConfiguration'; import { LanguageIdentifier, LanguageId } from 'vs/editor/common/modes'; /** @@ -55,6 +55,7 @@ export class RichEditSupport { public readonly indentRulesSupport: IndentRulesSupport; public readonly brackets: RichEditBrackets; public readonly indentationRules: IndentationRule; + public readonly foldingRules: FoldingRules; constructor(languageIdentifier: LanguageIdentifier, previous: RichEditSupport, rawConf: LanguageConfiguration) { @@ -82,6 +83,8 @@ export class RichEditSupport { if (this._conf.indentationRules) { this.indentRulesSupport = new IndentRulesSupport(this._conf.indentationRules); } + + this.foldingRules = this._conf.folding || {}; } private static _mergeConf(prev: LanguageConfiguration, current: LanguageConfiguration): LanguageConfiguration { @@ -93,6 +96,7 @@ export class RichEditSupport { onEnterRules: (prev ? current.onEnterRules || prev.onEnterRules : current.onEnterRules), autoClosingPairs: (prev ? current.autoClosingPairs || prev.autoClosingPairs : current.autoClosingPairs), surroundingPairs: (prev ? current.surroundingPairs || prev.surroundingPairs : current.surroundingPairs), + folding: (prev ? current.folding || prev.folding : current.folding), __electricCharacterSupport: (prev ? current.__electricCharacterSupport || prev.__electricCharacterSupport : current.__electricCharacterSupport), }; } @@ -143,12 +147,16 @@ export class RichEditSupport { } } +export class LanguageConfigurationChangeEvent { + languageIdentifier: LanguageIdentifier; +} + export class LanguageConfigurationRegistryImpl { private _entries: RichEditSupport[]; - private _onDidChange: Emitter = new Emitter(); - public onDidChange: Event = this._onDidChange.event; + private _onDidChange: Emitter = new Emitter(); + public onDidChange: Event = this._onDidChange.event; constructor() { this._entries = []; @@ -158,12 +166,12 @@ export class LanguageConfigurationRegistryImpl { let previous = this._getRichEditSupport(languageIdentifier.id); let current = new RichEditSupport(languageIdentifier, previous, configuration); this._entries[languageIdentifier.id] = current; - this._onDidChange.fire(void 0); + this._onDidChange.fire({ languageIdentifier }); return { dispose: () => { if (this._entries[languageIdentifier.id] === current) { this._entries[languageIdentifier.id] = previous; - this._onDidChange.fire(void 0); + this._onDidChange.fire({ languageIdentifier }); } } }; @@ -268,9 +276,15 @@ export class LanguageConfigurationRegistryImpl { return ensureValidWordDefinition(value.wordDefinition || null); } + public getFoldingRules(languageId: LanguageId): FoldingRules { + let value = this._getRichEditSupport(languageId); + if (!value) { + return {}; + } + return value.foldingRules; + } - - // beigin Indent Rules + // begin Indent Rules public getIndentRulesSupport(languageId: LanguageId): IndentRulesSupport { let value = this._getRichEditSupport(languageId); diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 4034473fa40..46244956a30 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -27,7 +27,8 @@ export const enum ViewEventType { ViewTokensChanged = 12, ViewTokensColorsChanged = 13, ViewZonesChanged = 14, - ViewThemeChanged = 15 + ViewThemeChanged = 15, + ViewLanguageConfigurationChanged = 16 } export class ViewConfigurationChangedEvent { @@ -282,6 +283,14 @@ export class ViewZonesChangedEvent { } } +export class ViewLanguageConfigurationEvent { + + public readonly type = ViewEventType.ViewLanguageConfigurationChanged; + + constructor() { + } +} + export type ViewEvent = ( ViewConfigurationChangedEvent | ViewCursorStateChangedEvent @@ -298,6 +307,7 @@ export type ViewEvent = ( | ViewTokensColorsChangedEvent | ViewZonesChangedEvent | ViewThemeChangedEvent + | ViewLanguageConfigurationEvent ); export interface IViewEventListener { diff --git a/src/vs/editor/common/viewModel/viewEventHandler.ts b/src/vs/editor/common/viewModel/viewEventHandler.ts index a3e03e9005d..874b206787e 100644 --- a/src/vs/editor/common/viewModel/viewEventHandler.ts +++ b/src/vs/editor/common/viewModel/viewEventHandler.ts @@ -49,6 +49,9 @@ export class ViewEventHandler extends Disposable { public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { return false; } + public onLanguageConfigurationChanged(e: viewEvents.ViewLanguageConfigurationEvent): boolean { + return false; + } public onLineMappingChanged(e: viewEvents.ViewLineMappingChangedEvent): boolean { return false; } @@ -121,6 +124,12 @@ export class ViewEventHandler extends Disposable { } break; + case viewEvents.ViewEventType.ViewLanguageConfigurationChanged: + if (this.onLanguageConfigurationChanged(e)) { + shouldRender = true; + } + break; + case viewEvents.ViewEventType.ViewLineMappingChanged: if (this.onLineMappingChanged(e)) { shouldRender = true; @@ -175,7 +184,6 @@ export class ViewEventHandler extends Disposable { } break; - case viewEvents.ViewEventType.ViewThemeChanged: if (this.onThemeChanged(e)) { shouldRender = true; diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index 20b51a429b4..13331cdc5ba 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -261,6 +261,10 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel // That's ok, a model tokens changed event will follow shortly break; } + case textModelEvents.TextModelEventType.ModelLanguageConfigurationChanged: { + eventsCollector.emit(new viewEvents.ViewLanguageConfigurationEvent()); + break; + } case textModelEvents.TextModelEventType.ModelContentChanged: { // Ignore break; diff --git a/src/vs/editor/contrib/folding/browser/folding.ts b/src/vs/editor/contrib/folding/browser/folding.ts index bbe359dd428..d6eba543ba4 100644 --- a/src/vs/editor/contrib/folding/browser/folding.ts +++ b/src/vs/editor/contrib/folding/browser/folding.ts @@ -237,6 +237,9 @@ export class FoldingController implements IFoldingController { this.localToDispose.push(this.contentChangedScheduler); this.localToDispose.push(this.cursorChangedScheduler); + this.localToDispose.push(model.onDidChangeLanguageConfiguration(e => { + this.contentChangedScheduler.schedule(); + })); this.localToDispose.push(this.editor.onDidChangeModelContent(e => this.contentChangedScheduler.schedule())); this.localToDispose.push(this.editor.onDidChangeCursorPosition((e) => { diff --git a/src/vs/editor/test/common/model/indentRanges.test.ts b/src/vs/editor/test/common/model/indentRanges.test.ts index 55b5e1ad6dd..6f13fd9c8f0 100644 --- a/src/vs/editor/test/common/model/indentRanges.test.ts +++ b/src/vs/editor/test/common/model/indentRanges.test.ts @@ -16,9 +16,9 @@ export interface IndentRange { } suite('Indentation Folding', () => { - function assertRanges(lines: string[], expected: IndentRange[]): void { + function assertRanges(lines: string[], expected: IndentRange[], offside): void { let model = Model.createFromString(lines.join('\n')); - let actual = computeRanges(model); + let actual = computeRanges(model, offside); actual.sort((r1, r2) => r1.startLineNumber - r2.startLineNumber); assert.deepEqual(actual, expected); model.dispose(); @@ -29,40 +29,48 @@ suite('Indentation Folding', () => { } test('Fold one level', () => { - assertRanges([ + let range = [ 'A', ' A', ' A', ' A' - ], [r(1, 4, 0)]); + ]; + assertRanges(range, [r(1, 4, 0)], true); + assertRanges(range, [r(1, 4, 0)], false); }); test('Fold two levels', () => { - assertRanges([ + let range = [ 'A', ' A', ' A', ' A', ' A' - ], [r(1, 5, 0), r(3, 5, 2)]); + ]; + assertRanges(range, [r(1, 5, 0), r(3, 5, 2)], true); + assertRanges(range, [r(1, 5, 0), r(3, 5, 2)], false); }); test('Fold three levels', () => { - assertRanges([ + let range = [ 'A', ' A', ' A', ' A', 'A' - ], [r(1, 4, 0), r(2, 4, 2), r(3, 4, 4)]); + ]; + assertRanges(range, [r(1, 4, 0), r(2, 4, 2), r(3, 4, 4)], true); + assertRanges(range, [r(1, 4, 0), r(2, 4, 2), r(3, 4, 4)], false); }); test('Fold decreasing indent', () => { - assertRanges([ + let range = [ ' A', ' A', 'A' - ], []); + ]; + assertRanges(range, [], true); + assertRanges(range, [], false); }); test('Fold Java', () => { @@ -80,7 +88,7 @@ suite('Indentation Folding', () => { /*11*/ 'interface B {', /*12*/ ' void bar();', /*13*/ '}', - ], [r(1, 9, 0), r(2, 4, 2), r(7, 8, 2), r(11, 12, 0)]); + ], [r(1, 9, 0), r(2, 4, 2), r(7, 8, 2), r(11, 12, 0)], false); }); test('Fold Javadoc', () => { @@ -92,9 +100,9 @@ suite('Indentation Folding', () => { /* 5*/ ' void foo() {', /* 6*/ ' }', /* 7*/ '}', - ], [r(1, 3, 0), r(4, 6, 0)]); + ], [r(1, 3, 0), r(4, 6, 0)], false); }); - test('Fold Whitespace', () => { + test('Fold Whitespace Java', () => { assertRanges([ /* 1*/ 'class A {', /* 2*/ '', @@ -104,7 +112,20 @@ suite('Indentation Folding', () => { /* 6*/ ' }', /* 7*/ ' ', /* 8*/ '}', - ], [r(1, 7, 0), r(3, 5, 2)]); + ], [r(1, 7, 0), r(3, 5, 2)], false); + }); + + test('Fold Whitespace Python', () => { + assertRanges([ + /* 1*/ 'def a:', + /* 2*/ ' pass', + /* 3*/ ' ', + /* 4*/ ' def b:', + /* 5*/ ' pass', + /* 6*/ ' ', + /* 7*/ ' ', + /* 8*/ 'def c: # since there was a deintent here' + ], [r(1, 5, 0), r(4, 5, 2)], true); }); test('Fold Tabs', () => { @@ -117,6 +138,6 @@ suite('Indentation Folding', () => { /* 6*/ ' \t}', /* 7*/ ' ', /* 8*/ '}', - ], [r(1, 7, 0), r(3, 5, 4)]); + ], [r(1, 7, 0), r(3, 5, 4)], false); }); }); diff --git a/src/vs/editor/test/common/model/textModel.test.ts b/src/vs/editor/test/common/model/textModel.test.ts index cba2a056eba..e36e11308e5 100644 --- a/src/vs/editor/test/common/model/textModel.test.ts +++ b/src/vs/editor/test/common/model/textModel.test.ts @@ -820,178 +820,4 @@ suite('TextModel.mightContainRTL', () => { assert.equal(model.mightContainRTL(), false); }); -}); - -suite('TextModel.getLineIndentGuide', () => { - function assertIndentGuides(lines: [number, string][]): void { - let text = lines.map(l => l[1]).join('\n'); - let model = TextModel.createFromString(text); - - let actual: [number, string][] = []; - for (let line = 1; line <= model.getLineCount(); line++) { - actual[line - 1] = [model.getLineIndentGuide(line), model.getLineContent(line)]; - } - - // let expected = lines.map(l => l[0]); - - assert.deepEqual(actual, lines); - - model.dispose(); - } - - test('getLineIndentGuide one level', () => { - assertIndentGuides([ - [0, 'A'], - [1, ' A'], - [1, ' A'], - [1, ' A'], - ]); - }); - - test('getLineIndentGuide two levels', () => { - assertIndentGuides([ - [0, 'A'], - [1, ' A'], - [1, ' A'], - [1, ' A'], - [1, ' A'], - ]); - }); - - test('getLineIndentGuide three levels', () => { - assertIndentGuides([ - [0, 'A'], - [1, ' A'], - [1, ' A'], - [2, ' A'], - [0, 'A'], - ]); - }); - - test('getLineIndentGuide decreasing indent', () => { - assertIndentGuides([ - [0, ' A'], - [0, ' A'], - [0, 'A'], - ]); - }); - - test('getLineIndentGuide Java', () => { - assertIndentGuides([ - /* 1*/[0, 'class A {'], - /* 2*/[1, ' void foo() {'], - /* 3*/[1, ' console.log(1);'], - /* 4*/[1, ' console.log(2);'], - /* 5*/[1, ' }'], - /* 6*/[1, ''], - /* 7*/[1, ' void bar() {'], - /* 8*/[1, ' console.log(3);'], - /* 9*/[1, ' }'], - /*10*/[0, '}'], - /*11*/[0, 'interface B {'], - /*12*/[1, ' void bar();'], - /*13*/[0, '}'], - ]); - }); - - test('getLineIndentGuide Javadoc', () => { - assertIndentGuides([ - [0, '/**'], - [1, ' * Comment'], - [1, ' */'], - [0, 'class A {'], - [1, ' void foo() {'], - [1, ' }'], - [0, '}'], - ]); - }); - - test('getLineIndentGuide Whitespace', () => { - assertIndentGuides([ - [0, 'class A {'], - [1, ''], - [1, ' void foo() {'], - [1, ' '], - [1, ' return 1;'], - [1, ' }'], - [1, ' '], - [0, '}'], - ]); - }); - - test('getLineIndentGuide Tabs', () => { - assertIndentGuides([ - [0, 'class A {'], - [1, '\t\t'], - [1, '\tvoid foo() {'], - [2, '\t \t//hello'], - [2, '\t return 2;'], - [1, ' \t}'], - [1, ' '], - [0, '}'], - ]); - }); - - test('getLineIndentGuide checker.ts', () => { - assertIndentGuides([ - /* 1*/[0, '/// '], - /* 2*/[0, ''], - /* 3*/[0, '/* @internal */'], - /* 4*/[0, 'namespace ts {'], - /* 5*/[1, ' let nextSymbolId = 1;'], - /* 6*/[1, ' let nextNodeId = 1;'], - /* 7*/[1, ' let nextMergeId = 1;'], - /* 8*/[1, ' let nextFlowId = 1;'], - /* 9*/[1, ''], - /*10*/[1, ' export function getNodeId(node: Node): number {'], - /*11*/[2, ' if (!node.id) {'], - /*12*/[3, ' node.id = nextNodeId;'], - /*13*/[3, ' nextNodeId++;'], - /*14*/[2, ' }'], - /*15*/[2, ' return node.id;'], - /*16*/[1, ' }'], - /*17*/[0, '}'], - ]); - }); - - test('issue #8425 - Missing indentation lines for first level indentation', () => { - assertIndentGuides([ - [1, '\tindent1'], - [2, '\t\tindent2'], - [2, '\t\tindent2'], - [1, '\tindent1'], - ]); - }); - - test('issue #8952 - Indentation guide lines going through text on .yml file', () => { - assertIndentGuides([ - [0, 'properties:'], - [1, ' emailAddress:'], - [2, ' - bla'], - [2, ' - length:'], - [3, ' max: 255'], - [0, 'getters:'], - ]); - }); - - test('issue #11892 - Indent guides look funny', () => { - assertIndentGuides([ - [0, 'function test(base) {'], - [1, '\tswitch (base) {'], - [2, '\t\tcase 1:'], - [3, '\t\t\treturn 1;'], - [2, '\t\tcase 2:'], - [3, '\t\t\treturn 2;'], - [1, '\t}'], - [0, '}'], - ]); - }); - - test('issue #12398 - Problem in indent guidelines', () => { - assertIndentGuides([ - [2, '\t\t.bla'], - [3, '\t\t\tlabel(for)'], - [0, 'include script'], - ]); - }); -}); +}); \ No newline at end of file diff --git a/src/vs/editor/test/common/model/textModelWithTokens.test.ts b/src/vs/editor/test/common/model/textModelWithTokens.test.ts index 6ac512096dd..24c66e08947 100644 --- a/src/vs/editor/test/common/model/textModelWithTokens.test.ts +++ b/src/vs/editor/test/common/model/textModelWithTokens.test.ts @@ -359,3 +359,177 @@ suite('TextModelWithTokens regression tests', () => { registration.dispose(); }); }); + +suite('TextModel.getLineIndentGuide', () => { + function assertIndentGuides(lines: [number, string][]): void { + let text = lines.map(l => l[1]).join('\n'); + let model = Model.createFromString(text); + + let actual: [number, string][] = []; + for (let line = 1; line <= model.getLineCount(); line++) { + actual[line - 1] = [model.getLineIndentGuide(line), model.getLineContent(line)]; + } + + // let expected = lines.map(l => l[0]); + + assert.deepEqual(actual, lines); + + model.dispose(); + } + + test('getLineIndentGuide one level', () => { + assertIndentGuides([ + [0, 'A'], + [1, ' A'], + [1, ' A'], + [1, ' A'], + ]); + }); + + test('getLineIndentGuide two levels', () => { + assertIndentGuides([ + [0, 'A'], + [1, ' A'], + [1, ' A'], + [1, ' A'], + [1, ' A'], + ]); + }); + + test('getLineIndentGuide three levels', () => { + assertIndentGuides([ + [0, 'A'], + [1, ' A'], + [1, ' A'], + [2, ' A'], + [0, 'A'], + ]); + }); + + test('getLineIndentGuide decreasing indent', () => { + assertIndentGuides([ + [0, ' A'], + [0, ' A'], + [0, 'A'], + ]); + }); + + test('getLineIndentGuide Java', () => { + assertIndentGuides([ + /* 1*/[0, 'class A {'], + /* 2*/[1, ' void foo() {'], + /* 3*/[1, ' console.log(1);'], + /* 4*/[1, ' console.log(2);'], + /* 5*/[1, ' }'], + /* 6*/[1, ''], + /* 7*/[1, ' void bar() {'], + /* 8*/[1, ' console.log(3);'], + /* 9*/[1, ' }'], + /*10*/[0, '}'], + /*11*/[0, 'interface B {'], + /*12*/[1, ' void bar();'], + /*13*/[0, '}'], + ]); + }); + + test('getLineIndentGuide Javadoc', () => { + assertIndentGuides([ + [0, '/**'], + [1, ' * Comment'], + [1, ' */'], + [0, 'class A {'], + [1, ' void foo() {'], + [1, ' }'], + [0, '}'], + ]); + }); + + test('getLineIndentGuide Whitespace', () => { + assertIndentGuides([ + [0, 'class A {'], + [1, ''], + [1, ' void foo() {'], + [1, ' '], + [1, ' return 1;'], + [1, ' }'], + [1, ' '], + [0, '}'], + ]); + }); + + test('getLineIndentGuide Tabs', () => { + assertIndentGuides([ + [0, 'class A {'], + [1, '\t\t'], + [1, '\tvoid foo() {'], + [2, '\t \t//hello'], + [2, '\t return 2;'], + [1, ' \t}'], + [1, ' '], + [0, '}'], + ]); + }); + + test('getLineIndentGuide checker.ts', () => { + assertIndentGuides([ + /* 1*/[0, '/// '], + /* 2*/[0, ''], + /* 3*/[0, '/* @internal */'], + /* 4*/[0, 'namespace ts {'], + /* 5*/[1, ' let nextSymbolId = 1;'], + /* 6*/[1, ' let nextNodeId = 1;'], + /* 7*/[1, ' let nextMergeId = 1;'], + /* 8*/[1, ' let nextFlowId = 1;'], + /* 9*/[1, ''], + /*10*/[1, ' export function getNodeId(node: Node): number {'], + /*11*/[2, ' if (!node.id) {'], + /*12*/[3, ' node.id = nextNodeId;'], + /*13*/[3, ' nextNodeId++;'], + /*14*/[2, ' }'], + /*15*/[2, ' return node.id;'], + /*16*/[1, ' }'], + /*17*/[0, '}'], + ]); + }); + + test('issue #8425 - Missing indentation lines for first level indentation', () => { + assertIndentGuides([ + [1, '\tindent1'], + [2, '\t\tindent2'], + [2, '\t\tindent2'], + [1, '\tindent1'], + ]); + }); + + test('issue #8952 - Indentation guide lines going through text on .yml file', () => { + assertIndentGuides([ + [0, 'properties:'], + [1, ' emailAddress:'], + [2, ' - bla'], + [2, ' - length:'], + [3, ' max: 255'], + [0, 'getters:'], + ]); + }); + + test('issue #11892 - Indent guides look funny', () => { + assertIndentGuides([ + [0, 'function test(base) {'], + [1, '\tswitch (base) {'], + [2, '\t\tcase 1:'], + [3, '\t\t\treturn 1;'], + [2, '\t\tcase 2:'], + [3, '\t\t\treturn 2;'], + [1, '\t}'], + [0, '}'], + ]); + }); + + test('issue #12398 - Problem in indent guidelines', () => { + assertIndentGuides([ + [2, '\t\t.bla'], + [3, '\t\t\tlabel(for)'], + [0, 'include script'], + ]); + }); +}); diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index e1b0f728c19..8be006d7a8c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -1787,6 +1787,11 @@ declare module monaco.editor { * @event */ onDidChangeLanguage(listener: (e: IModelLanguageChangedEvent) => void): IDisposable; + /** + * An event emitted when the language configuration associated with the model has changed. + * @event + */ + onDidChangeLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) => void): IDisposable; /** * An event emitted right before disposing the model. * @event @@ -2185,6 +2190,11 @@ declare module monaco.editor { * @event */ onDidChangeModelLanguage(listener: (e: IModelLanguageChangedEvent) => void): IDisposable; + /** + * An event emitted when the language configuration of the current model has changed. + * @event + */ + onDidChangeModelLanguageConfiguration(listener: (e: IModelLanguageConfigurationChangedEvent) => void): IDisposable; /** * An event emitted when the options of the current model has changed. * @event @@ -2419,6 +2429,12 @@ declare module monaco.editor { readonly newLanguage: string; } + /** + * An event describing that the language configuration associated with a model has changed. + */ + export interface IModelLanguageConfigurationChangedEvent { + } + export interface IModelContentChange { /** * The range that got replaced. @@ -4294,6 +4310,10 @@ declare module monaco.languages { * settings will be used. */ surroundingPairs?: IAutoClosingPair[]; + /** + * The language's folding rules. + */ + folding?: FoldingRules; /** * **Deprecated** Do not use. * @@ -4324,6 +4344,21 @@ declare module monaco.languages { unIndentedLinePattern?: RegExp; } + /** + * Describes folding rules for a language. + */ + export interface FoldingRules { + indendationBasedFolding?: { + /** + * Used by the indentation based strategy to decide wheter empty lines belong to the previous or the next block. + * A language adheres to the off-side rule if blocks in that language are expressed by their indentation. + * See [wikipedia](https://en.wikipedia.org/wiki/Off-side_rule) for more information. + * If not set, `false` is used and empty lines belong to the previous block. + */ + offSide: boolean; + }; + } + /** * Describes a rule to be evaluated when pressing Enter. */ diff --git a/src/vs/platform/workspaces/common/workspaces.ts b/src/vs/platform/workspaces/common/workspaces.ts index 44d42a64d14..bd9d29dc731 100644 --- a/src/vs/platform/workspaces/common/workspaces.ts +++ b/src/vs/platform/workspaces/common/workspaces.ts @@ -14,6 +14,7 @@ import { isLinux } from 'vs/base/common/platform'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import Event from 'vs/base/common/event'; import { tildify, getPathLabel } from 'vs/base/common/labels'; +import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; export const IWorkspacesMainService = createDecorator('workspacesMainService'); export const IWorkspacesService = createDecorator('workspacesService'); @@ -37,12 +38,10 @@ export interface IStoredWorkspaceFolder { name?: string; } -export interface IStoredWorkspace { - folders: IStoredWorkspaceFolder[]; +export interface IResolvedWorkspace extends IWorkspaceIdentifier { + folders: WorkspaceFolder[]; } -export interface IResolvedWorkspace extends IWorkspaceIdentifier, IStoredWorkspace { } - export interface IWorkspaceSavedEvent { workspace: IWorkspaceIdentifier; oldConfigPath: string; diff --git a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts index c5b552022a3..5f40e430e13 100644 --- a/src/vs/platform/workspaces/electron-main/workspacesMainService.ts +++ b/src/vs/platform/workspaces/electron-main/workspacesMainService.ts @@ -5,7 +5,7 @@ 'use strict'; -import { IWorkspacesMainService, IWorkspaceIdentifier, IStoredWorkspace, WORKSPACE_EXTENSION, IWorkspaceSavedEvent, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace } from 'vs/platform/workspaces/common/workspaces'; +import { IWorkspacesMainService, IWorkspaceIdentifier, WORKSPACE_EXTENSION, IWorkspaceSavedEvent, UNTITLED_WORKSPACE_NAME, IResolvedWorkspace, IStoredWorkspaceFolder } from 'vs/platform/workspaces/common/workspaces'; import { TPromise } from 'vs/base/common/winjs.base'; import { isParent } from 'vs/platform/files/common/files'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; @@ -23,6 +23,11 @@ import * as json from 'vs/base/common/json'; import * as jsonEdit from 'vs/base/common/jsonEdit'; import { applyEdit } from 'vs/base/common/jsonFormatter'; import { massageFolderPathForWorkspace } from 'vs/platform/workspaces/node/workspaces'; +import { toWorkspaceFolders } from 'vs/platform/workspace/common/workspace'; + +export interface IStoredWorkspace { + folders: IStoredWorkspaceFolder[]; +} export class WorkspacesMainService implements IWorkspacesMainService { @@ -85,7 +90,7 @@ export class WorkspacesMainService implements IWorkspacesMainService { return { id: this.getWorkspaceId(path), configPath: path, - folders: workspace.folders + folders: toWorkspaceFolders(workspace.folders) }; } catch (error) { this.logService.log(error.toString()); diff --git a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts index 03bddbec9e3..fa958affd34 100644 --- a/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts +++ b/src/vs/platform/workspaces/test/electron-main/workspacesMainService.test.ts @@ -13,8 +13,8 @@ import extfs = require('vs/base/node/extfs'); import pfs = require('vs/base/node/pfs'); import { EnvironmentService } from 'vs/platform/environment/node/environmentService'; import { parseArgs } from 'vs/platform/environment/node/argv'; -import { WorkspacesMainService } from 'vs/platform/workspaces/electron-main/workspacesMainService'; -import { IStoredWorkspace, WORKSPACE_EXTENSION, IWorkspaceSavedEvent, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; +import { WorkspacesMainService, IStoredWorkspace } from 'vs/platform/workspaces/electron-main/workspacesMainService'; +import { WORKSPACE_EXTENSION, IWorkspaceSavedEvent, IWorkspaceIdentifier } from 'vs/platform/workspaces/common/workspaces'; import { LogMainService } from 'vs/platform/log/common/log'; import URI from 'vs/base/common/uri'; @@ -138,7 +138,7 @@ suite('WorkspacesMainService', () => { fs.writeFileSync(workspace.configPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib' }] })); const resolved = service.resolveWorkspaceSync(workspace.configPath); - assert.equal(URI.file(resolved.folders[0].path).fsPath, URI.file(path.join(path.dirname(workspace.configPath), 'ticino-playground', 'lib')).fsPath); + assert.equal(resolved.folders[0].uri.fsPath, URI.file(path.join(path.dirname(workspace.configPath), 'ticino-playground', 'lib')).fsPath); done(); }); @@ -149,7 +149,7 @@ suite('WorkspacesMainService', () => { fs.writeFileSync(workspace.configPath, JSON.stringify({ folders: [{ path: './ticino-playground/lib/../other' }] })); const resolved = service.resolveWorkspaceSync(workspace.configPath); - assert.equal(URI.file(resolved.folders[0].path).fsPath, URI.file(path.join(path.dirname(workspace.configPath), 'ticino-playground', 'other')).fsPath); + assert.equal(resolved.folders[0].uri.fsPath, URI.file(path.join(path.dirname(workspace.configPath), 'ticino-playground', 'other')).fsPath); done(); }); @@ -160,7 +160,7 @@ suite('WorkspacesMainService', () => { fs.writeFileSync(workspace.configPath, JSON.stringify({ folders: [{ path: 'ticino-playground/lib' }] })); const resolved = service.resolveWorkspaceSync(workspace.configPath); - assert.equal(URI.file(resolved.folders[0].path).fsPath, URI.file(path.join(path.dirname(workspace.configPath), 'ticino-playground', 'lib')).fsPath); + assert.equal(resolved.folders[0].uri.fsPath, URI.file(path.join(path.dirname(workspace.configPath), 'ticino-playground', 'lib')).fsPath); done(); }); @@ -171,7 +171,7 @@ suite('WorkspacesMainService', () => { fs.writeFileSync(workspace.configPath, '{ "folders": [ { "path": "./ticino-playground/lib" } , ] }'); // trailing comma const resolved = service.resolveWorkspaceSync(workspace.configPath); - assert.equal(URI.file(resolved.folders[0].path).fsPath, URI.file(path.join(path.dirname(workspace.configPath), 'ticino-playground', 'lib')).fsPath); + assert.equal(resolved.folders[0].uri.fsPath, URI.file(path.join(path.dirname(workspace.configPath), 'ticino-playground', 'lib')).fsPath); done(); }); diff --git a/src/vs/workbench/browser/parts/views/views2.ts b/src/vs/workbench/browser/parts/views/panelViewlet.ts similarity index 97% rename from src/vs/workbench/browser/parts/views/views2.ts rename to src/vs/workbench/browser/parts/views/panelViewlet.ts index 1721592a293..867209cab85 100644 --- a/src/vs/workbench/browser/parts/views/views2.ts +++ b/src/vs/workbench/browser/parts/views/panelViewlet.ts @@ -129,11 +129,6 @@ export class PanelViewlet extends Viewlet { private panelItems: IViewletPanelItem[] = []; private panelview: PanelView; - // TODO@Joao make this into method so people can override it - protected get isSingleView(): boolean { - return this.options.showHeaderInTitleWhenSingleView && this.panelItems.length === 1; - } - protected get length(): number { return this.panelItems.length; } @@ -157,7 +152,7 @@ export class PanelViewlet extends Viewlet { getTitle(): string { let title = Registry.as(Extensions.Viewlets).getViewlet(this.getId()).name; - if (this.isSingleView) { + if (this.isSingleView()) { title += ': ' + this.panelItems[0].panel.title; } @@ -165,7 +160,7 @@ export class PanelViewlet extends Viewlet { } getActions(): IAction[] { - if (this.isSingleView) { + if (this.isSingleView()) { return this.panelItems[0].panel.getActions(); } @@ -173,7 +168,7 @@ export class PanelViewlet extends Viewlet { } getSecondaryActions(): IAction[] { - if (this.isSingleView) { + if (this.isSingleView()) { return this.panelItems[0].panel.getSecondaryActions(); } @@ -257,13 +252,17 @@ export class PanelViewlet extends Viewlet { } private updateViewHeaders(): void { - if (this.isSingleView) { + if (this.isSingleView()) { this.panelItems[0].panel.headerVisible = false; } else { this.panelItems.forEach(i => i.panel.headerVisible = true); } } + protected isSingleView(): boolean { + return this.options.showHeaderInTitleWhenSingleView && this.panelItems.length === 1; + } + dispose(): void { super.dispose(); this.panelItems.forEach(i => i.disposable.dispose()); diff --git a/src/vs/workbench/parts/codeEditor/electron-browser/languageConfiguration/languageConfigurationExtensionPoint.ts b/src/vs/workbench/parts/codeEditor/electron-browser/languageConfiguration/languageConfigurationExtensionPoint.ts index c5ba3a10a1b..b97d7b652e3 100644 --- a/src/vs/workbench/parts/codeEditor/electron-browser/languageConfiguration/languageConfigurationExtensionPoint.ts +++ b/src/vs/workbench/parts/codeEditor/electron-browser/languageConfiguration/languageConfigurationExtensionPoint.ts @@ -7,7 +7,7 @@ import * as nls from 'vs/nls'; import { parse, ParseError } from 'vs/base/common/json'; import { readFile } from 'vs/base/node/pfs'; -import { CharacterPair, LanguageConfiguration, IAutoClosingPair, IAutoClosingPairConditional, IndentationRule, CommentRule } from 'vs/editor/common/modes/languageConfiguration'; +import { CharacterPair, LanguageConfiguration, IAutoClosingPair, IAutoClosingPairConditional, IndentationRule, CommentRule, FoldingRules } from 'vs/editor/common/modes/languageConfiguration'; import { IModeService } from 'vs/editor/common/services/modeService'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { Extensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry'; @@ -35,6 +35,7 @@ interface ILanguageConfiguration { surroundingPairs?: (CharacterPair | IAutoClosingPair)[]; wordPattern?: string | IRegExp; indentationRules?: IIndentationRules; + folding?: FoldingRules; } export class LanguageConfigurationFileHandler { @@ -117,6 +118,10 @@ export class LanguageConfigurationFileHandler { } } + if (configuration.folding) { + richEditConfig.folding = configuration.folding; + } + LanguageConfigurationRegistry.register(languageIdentifier, richEditConfig); } @@ -377,7 +382,24 @@ const schema: IJSONSchema = { } } } + }, + folding: { + type: 'object', + description: nls.localize('schema.folding', 'The language\'s folding settings.'), + properties: { + indendationBasedFolding: { + type: 'object', + description: nls.localize('schema.folding.indendationBasedFolding', 'Settings for indentation based folding.'), + properties: { + offSide: { + type: 'boolean', + description: nls.localize('schema.folding.indendationBasedFolding.offSide', 'A language adheres to the off-side rule if blocks in that language are expressed by their indentation. If set, empty lines belong to the subsequent block.'), + } + } + } + } } + } }; let schemaRegistry = Registry.as(Extensions.JSONContribution); diff --git a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts index 311b186a859..d631f79db21 100644 --- a/src/vs/workbench/parts/extensions/browser/extensionsActions.ts +++ b/src/vs/workbench/parts/extensions/browser/extensionsActions.ts @@ -26,19 +26,19 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { Query } from 'vs/workbench/parts/extensions/common/extensionQuery'; import { IFileService, IContent } from 'vs/platform/files/common/files'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IWindowService } from 'vs/platform/windows/common/windows'; import { IExtensionService, IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import URI from 'vs/base/common/uri'; -import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; import { buttonBackground, buttonForeground, buttonHoverBackground, contrastBorder, registerColor, foreground } from 'vs/platform/theme/common/colorRegistry'; import { Color } from 'vs/base/common/color'; -import { IPickOpenEntry, IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen'; import { IJSONEditingService } from 'vs/workbench/services/configuration/common/jsonEditing'; import { ITextEditorSelection } from 'vs/platform/editor/common/editor'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; +import { PICK_WORKSPACE_FOLDER_COMMAND } from 'vs/workbench/browser/actions/workspaceActions'; export class InstallAction extends Action { @@ -1393,12 +1393,12 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac constructor( id: string, label: string, - @IQuickOpenService private quickOpenService: IQuickOpenService, @IFileService fileService: IFileService, @IWorkspaceContextService contextService: IWorkspaceContextService, @IWorkbenchEditorService editorService: IWorkbenchEditorService, @IJSONEditingService jsonEditingService: IJSONEditingService, - @ITextModelService textModelResolverService: ITextModelService + @ITextModelService textModelResolverService: ITextModelService, + @ICommandService private commandService: ICommandService ) { super(id, label, contextService, fileService, editorService, jsonEditingService, textModelResolverService); this.contextService.onDidChangeWorkspaceFolders(() => this.update(), this, this.disposables); @@ -1410,19 +1410,12 @@ export class ConfigureWorkspaceFolderRecommendedExtensionsAction extends Abstrac } public run(): TPromise { - const picks: IPickOpenEntry[] = this.contextService.getWorkspace().folders.map((folder, index) => { - return { - label: folder.name, - id: `${index}` - }; - }); - - return this.quickOpenService.pick(picks, { placeHolder: localize('pickFolder', "Select Workspace Folder") }) - .then(pick => { - if (pick) { - return this.openExtensionsFile(this.contextService.toResource(paths.join('.vscode', 'extensions.json'), this.contextService.getWorkspace().folders[parseInt(pick.id)])); + return this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND) + .then(workspaceFolder => { + if (workspaceFolder) { + return this.openExtensionsFile(this.contextService.toResource(paths.join('.vscode', 'extensions.json'), workspaceFolder)); } - return undefined; + return null; }); } diff --git a/src/vs/workbench/parts/files/browser/views/explorerView.ts b/src/vs/workbench/parts/files/browser/views/explorerView.ts index dd5570cf3db..b2b8d890596 100644 --- a/src/vs/workbench/parts/files/browser/views/explorerView.ts +++ b/src/vs/workbench/parts/files/browser/views/explorerView.ts @@ -32,7 +32,7 @@ import { FileStat, Model } from 'vs/workbench/parts/files/common/explorerModel'; import { IListService } from 'vs/platform/list/browser/listService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IPartService } from 'vs/workbench/services/part/common/partService'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; +import { IWorkspaceContextService, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -166,7 +166,7 @@ export class ExplorerView extends CollapsibleView { }; this.toDispose.push(this.themeService.onDidFileIconThemeChange(onFileIconThemeChange)); - this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders(() => this.refreshFromEvent())); + this.toDispose.push(this.contextService.onDidChangeWorkspaceFolders((e) => this.refreshFromEvent(e.added))); onFileIconThemeChange(this.themeService.getFileIconTheme()); } @@ -194,7 +194,11 @@ export class ExplorerView extends CollapsibleView { this.onConfigurationUpdated(configuration); // Load and Fill Viewer - return this.doRefresh().then(() => { + let targetsToExpand = []; + if (this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES]) { + targetsToExpand = this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES].map((e: string) => URI.parse(e)); + } + return this.doRefresh(targetsToExpand).then(() => { // When the explorer viewer is loaded, listen to changes to the editor input this.toDispose.push(this.editorGroupService.onEditorsChanged(() => this.onEditorsChanged())); @@ -676,11 +680,11 @@ export class ExplorerView extends CollapsibleView { })); } - private refreshFromEvent(): void { + private refreshFromEvent(newRoots: WorkspaceFolder[] = []): void { if (this.isVisible()) { this.explorerRefreshDelayer.trigger(() => { if (!this.explorerViewer.getHighlight()) { - return this.doRefresh(); + return this.doRefresh(newRoots.map(r => r.uri)); } return TPromise.as(null); @@ -722,20 +726,13 @@ export class ExplorerView extends CollapsibleView { }); } - private doRefresh(): TPromise { + private doRefresh(targetsToExpand: URI[] = []): TPromise { const targetsToResolve: { root: FileStat, resource: URI, options: { resolveTo: URI[] } }[] = []; this.model.roots.forEach(root => { const rootAndTargets = { root, resource: root.resource, options: { resolveTo: [] } }; targetsToResolve.push(rootAndTargets); }); - let targetsToExpand: URI[] = []; - if (this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES]) { - targetsToExpand = this.settings[ExplorerView.MEMENTO_EXPANDED_FOLDER_RESOURCES].map((e: string) => URI.parse(e)); - } else if (this.model.roots.length === 1) { - targetsToExpand = this.model.roots.map(root => root.resource); // always expand if there is just one root - } - // First time refresh: Receive target through active editor input or selection and also include settings from previous session if (!this.isCreated) { const activeFile = this.getActiveFile(); @@ -783,17 +780,12 @@ export class ExplorerView extends CollapsibleView { // Subsequent refresh: Merge stat into our local model and refresh tree modelStats.forEach((modelStat, index) => FileStat.mergeLocalWithDisk(modelStat, this.model.roots[index])); - let input = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? this.model.roots[0] : this.model; + const input = this.contextService.getWorkbenchState() === WorkbenchState.FOLDER ? this.model.roots[0] : this.model; + const statsToExpand = this.explorerViewer.getExpandedElements().concat(targetsToExpand.map(target => this.model.findClosest(target))); if (input === this.explorerViewer.getInput()) { - return this.explorerViewer.refresh(); + return this.explorerViewer.refresh().then(() => this.explorerViewer.expandAll(statsToExpand)); } - // Preserve expanded elements if tree input changed. - // If it is a brand new tree just expand elements from memento - const expanded = this.explorerViewer.getExpandedElements(); - const statsToExpand = expanded.length ? [this.model.roots[0]].concat(expanded) : - targetsToExpand.map(expand => this.model.findClosest(expand)); - // Display roots only when multi folder workspace // Make sure to expand all folders that where expanded in the previous session return this.explorerViewer.setInput(input).then(() => this.explorerViewer.expandAll(statsToExpand)); diff --git a/src/vs/workbench/parts/preferences/browser/preferencesActions.ts b/src/vs/workbench/parts/preferences/browser/preferencesActions.ts index 0417094813d..c1e222a04dd 100644 --- a/src/vs/workbench/parts/preferences/browser/preferencesActions.ts +++ b/src/vs/workbench/parts/preferences/browser/preferencesActions.ts @@ -11,9 +11,10 @@ import { Action } from 'vs/base/common/actions'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IQuickOpenService, IPickOpenEntry, IFilePickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; -import { IPreferencesService, getSettingsTargetName } from 'vs/workbench/parts/preferences/common/preferences'; -import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace'; -import { ConfigurationTarget } from 'vs/workbench/services/configuration/common/configurationEditing'; +import { IPreferencesService } from 'vs/workbench/parts/preferences/common/preferences'; +import { IWorkspaceContextService, WorkbenchState, WorkspaceFolder } from 'vs/platform/workspace/common/workspace'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { PICK_WORKSPACE_FOLDER_COMMAND } from 'vs/workbench/browser/actions/workspaceActions'; export class OpenGlobalSettingsAction extends Action { @@ -114,7 +115,7 @@ export class OpenFolderSettingsAction extends Action { label: string, @IPreferencesService private preferencesService: IPreferencesService, @IWorkspaceContextService private workspaceContextService: IWorkspaceContextService, - @IQuickOpenService private quickOpenService: IQuickOpenService + @ICommandService private commandService: ICommandService ) { super(id, label); this.update(); @@ -127,21 +128,13 @@ export class OpenFolderSettingsAction extends Action { } public run(): TPromise { - const picks: IPickOpenEntry[] = this.workspaceContextService.getWorkspace().folders.map((folder, index) => { - return { - label: getSettingsTargetName(ConfigurationTarget.FOLDER, folder.uri, this.workspaceContextService), - id: `${index}` - }; - }); - - return this.quickOpenService.pick(picks, { placeHolder: nls.localize('pickFolder', "Select Folder") }) - .then(pick => { - if (pick) { - return this.preferencesService.openFolderSettings(this.workspaceContextService.getWorkspace().folders[parseInt(pick.id)].uri); + return this.commandService.executeCommand(PICK_WORKSPACE_FOLDER_COMMAND) + .then(workspaceFolder => { + if (workspaceFolder) { + return this.preferencesService.openFolderSettings(workspaceFolder.uri); } - return undefined; + return null; }); - } public dispose(): void { diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index c88fb426b53..15d3c109f1a 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -13,7 +13,7 @@ import { basename } from 'vs/base/common/paths'; import { onUnexpectedError } from 'vs/base/common/errors'; import { IDisposable, dispose, combinedDisposable, empty as EmptyDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { Builder, Dimension } from 'vs/base/browser/builder'; -import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/views2'; +import { PanelViewlet, ViewletPanel } from 'vs/workbench/browser/parts/views/panelViewlet'; import { append, $, addClass, toggleClass, trackFocus } from 'vs/base/browser/dom'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { List } from 'vs/base/browser/ui/list/listWidget'; @@ -789,7 +789,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { } getActions(): IAction[] { - if (this.isSingleView && this.repositories.length === 1) { + if (this.isSingleView()) { const [panel] = this.repositoryPanels; return panel.getActions(); } @@ -800,7 +800,7 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { getSecondaryActions(): IAction[] { let result: IAction[]; - if (this.isSingleView && this.repositories.length === 1) { + if (this.isSingleView()) { const [panel] = this.repositoryPanels; result = [ @@ -865,6 +865,10 @@ export class SCMViewlet extends PanelViewlet implements IViewModel { } } + protected isSingleView(): boolean { + return super.isSingleView() && this.repositories.length === 1; + } + dispose(): void { this.disposables = dispose(this.disposables); this.mainPanelDisposable.dispose(); diff --git a/src/vs/workbench/services/history/browser/history.ts b/src/vs/workbench/services/history/browser/history.ts index 2b8dedeb038..8e8e0c3efee 100644 --- a/src/vs/workbench/services/history/browser/history.ts +++ b/src/vs/workbench/services/history/browser/history.ts @@ -720,7 +720,10 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic if (input instanceof EditorInput) { const factory = registry.getEditorInputFactory(input.getTypeId()); if (factory) { - return { editorInputJSON: { typeId: input.getTypeId(), deserialized: factory.serialize(input) } } as ISerializedEditorHistoryEntry; + const deserialized = factory.serialize(input); + if (deserialized) { + return { editorInputJSON: { typeId: input.getTypeId(), deserialized } } as ISerializedEditorHistoryEntry; + } } } @@ -754,10 +757,11 @@ export class HistoryService extends BaseHistoryService implements IHistoryServic } // Editor input: via factory - if (serializedEditorHistoryEntry.editorInputJSON) { - const factory = registry.getEditorInputFactory(serializedEditorHistoryEntry.editorInputJSON.typeId); + const { editorInputJSON } = serializedEditorHistoryEntry; + if (editorInputJSON && editorInputJSON.deserialized) { + const factory = registry.getEditorInputFactory(editorInputJSON.typeId); if (factory) { - return factory.deserialize(this.instantiationService, serializedEditorHistoryEntry.editorInputJSON.deserialized); + return factory.deserialize(this.instantiationService, editorInputJSON.deserialized); } }