diff --git a/extensions/git/package.json b/extensions/git/package.json index 03e4423cd88..bd055ca12ee 100644 --- a/extensions/git/package.json +++ b/extensions/git/package.json @@ -94,6 +94,11 @@ "dark": "resources/icons/dark/clean.svg" } }, + { + "command": "git.commit", + "title": "%command.commit%", + "category": "Git" + }, { "command": "git.commitStaged", "title": "%command.commitStaged%", @@ -165,6 +170,14 @@ "category": "Git" } ], + "keybindings": [ + { + "command": "git.commitWithInput", + "key": "ctrl+enter", + "mac": "cmd+enter", + "when": "inSCMInput" + } + ], "menus": { "scm/title": [ { diff --git a/extensions/git/package.nls.json b/extensions/git/package.nls.json index 01aa344df75..ea998eabbb9 100644 --- a/extensions/git/package.nls.json +++ b/extensions/git/package.nls.json @@ -8,6 +8,7 @@ "command.unstageAll": "Unstage All", "command.clean": "Clean", "command.cleanAll": "Clean All", + "command.commit": "Commit", "command.commitStaged": "Commit Staged", "command.commitStagedSigned": "Commit Staged (Signed Off)", "command.commitAll": "Commit All", diff --git a/extensions/git/src/commands.ts b/extensions/git/src/commands.ts index 83ca45aa613..5185a6155b8 100644 --- a/extensions/git/src/commands.ts +++ b/extensions/git/src/commands.ts @@ -8,6 +8,7 @@ import { Uri, commands, scm, Disposable, SCMResourceGroup, SCMResource, window, workspace, QuickPickItem, OutputChannel } from 'vscode'; import { IRef, RefType } from './git'; import { Model, Resource, Status } from './model'; +import { CommitController } from './commit'; import * as path from 'path'; import * as nls from 'vscode-nls'; @@ -125,7 +126,11 @@ export class CommandCenter { private disposables: Disposable[]; - constructor(private model: Model, private outputChannel: OutputChannel) { + constructor( + private model: Model, + private commitController: CommitController, + private outputChannel: OutputChannel + ) { this.disposables = CommandCenter.Commands .map(({ commandId, method }) => commands.registerCommand(commandId, method, this)); } @@ -286,7 +291,7 @@ export class CommandCenter { return; } - return await this.model.clean(resource); + await this.model.clean(resource); } @CommandCenter.Command('git.cleanAll') @@ -301,13 +306,45 @@ export class CommandCenter { return; } - return await this.model.clean(...this.model.workingTreeGroup.resources); + await this.model.clean(...this.model.workingTreeGroup.resources); } - @CommandCenter.CatchErrors - async commit(message: string): Promise { + private async _commit(fn: () => Promise): Promise { + if (this.model.indexGroup.resources.length === 0 && this.model.workingTreeGroup.resources.length === 0) { + window.showInformationMessage(localize('no changes', "There are no changes to commit.")); + return false; + } + + const message = await fn(); + + if (!message) { + // TODO@joao: show modal dialog to confirm empty message commit + return false; + } + const all = this.model.indexGroup.resources.length === 0; - return this.model.commit(message, { all }); + await this.model.commit(message, { all }); + + return true; + } + + @CommandCenter.Command('git.commit') + @CommandCenter.CatchErrors + async commit(): Promise { + await this._commit(async () => await window.showInputBox({ + placeHolder: localize('commit message', "Commit message"), + prompt: localize('provide commit message', "Please provide a commit message") + })); + } + + @CommandCenter.Command('git.commitWithInput') + @CommandCenter.CatchErrors + async commitWithInput(): Promise { + const didCommit = await this._commit(async () => this.commitController.message); + + if (didCommit) { + this.commitController.message = ''; + } } @CommandCenter.Command('git.commitStaged') diff --git a/extensions/git/src/commit.ts b/extensions/git/src/commit.ts new file mode 100644 index 00000000000..7010838f6f7 --- /dev/null +++ b/extensions/git/src/commit.ts @@ -0,0 +1,112 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { workspace, window, languages, Disposable, Uri, TextDocumentChangeEvent, HoverProvider, Hover, TextEditor, Position, TextDocument, Range, TextEditorDecorationType, WorkspaceEdit } from 'vscode'; +import { Model } from './model'; +import { filterEvent } from './util'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); +const scmInputUri = Uri.parse('scm:input'); + +function isSCMInput(uri: Uri) { + return uri.toString() === scmInputUri.toString(); +} + +interface Diagnostic { + range: Range; + message: string; +} + +// TODO@Joao: hover dissapears if editor is scrolled +export class CommitController implements HoverProvider { + + private visibleTextEditorsDisposable: Disposable; + private editor: TextEditor; + private diagnostics: Diagnostic[] = []; + private decorationType: TextEditorDecorationType; + private disposables: Disposable[] = []; + + get message(): string | undefined { + if (!this.editor) { + return; + } + + return this.editor.document.getText(); + } + + set message(message: string | undefined) { + if (!this.editor || message === undefined) { + return; + } + + const document = this.editor.document; + const start = document.lineAt(0).range.start; + const end = document.lineAt(document.lineCount - 1).range.end; + const range = new Range(start, end); + const edit = new WorkspaceEdit(); + edit.replace(scmInputUri, range, message); + workspace.applyEdit(edit); + } + + constructor(private model: Model) { + this.visibleTextEditorsDisposable = window.onDidChangeVisibleTextEditors(this.onVisibleTextEditors, this); + this.onVisibleTextEditors(window.visibleTextEditors); + + this.decorationType = window.createTextEditorDecorationType({ + isWholeLine: true, + color: 'rgb(228, 157, 43)', + dark: { + color: 'rgb(220, 211, 71)' + } + }); + } + + private onVisibleTextEditors(editors: TextEditor[]): void { + const [editor] = editors.filter(e => isSCMInput(e.document.uri)); + + if (!editor) { + return; + } + + this.visibleTextEditorsDisposable.dispose(); + this.editor = editor; + + const onDidChange = filterEvent(workspace.onDidChangeTextDocument, e => e.document && isSCMInput(e.document.uri)); + onDidChange(this.onSCMInputChange, this, this.disposables); + + languages.registerHoverProvider({ scheme: 'scm' }, this); + } + + private onSCMInputChange(e: TextDocumentChangeEvent): void { + this.diagnostics = []; + + const range = e.document.lineAt(0).range; + const length = range.end.character - range.start.character; + + if (length > 80) { + const message = localize('too long', "You should keep the first line under 50 characters.\n\nYou can use more lines for extra information."); + this.diagnostics.push({ range, message }); + } + + this.editor.setDecorations(this.decorationType, this.diagnostics.map(d => d.range)); + } + + provideHover(document: TextDocument, position: Position): Hover | undefined { + const [decoration] = this.diagnostics.filter(d => d.range.contains(position)); + + if (!decoration) { + return; + } + + return new Hover(decoration.message, decoration.range); + } + + dispose(): void { + this.disposables.forEach(d => d.dispose()); + } +} diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index 1df0364a6ad..61b145296be 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -15,6 +15,7 @@ import { filterEvent, anyEvent } from './util'; import { GitContentProvider } from './contentProvider'; import { AutoFetcher } from './autofetch'; import { MergeDecorator } from './merge'; +import { CommitController } from './commit'; import * as nls from 'vscode-nls'; const localize = nls.config()(); @@ -41,7 +42,8 @@ async function init(disposables: Disposable[]): Promise { outputChannel.appendLine(localize('using git', "Using git {0} from {1}", info.version, info.path)); git.onOutput(str => outputChannel.append(str), null, disposables); - const commandCenter = new CommandCenter(model, outputChannel); + const commitHandler = new CommitController(model); + const commandCenter = new CommandCenter(model, commitHandler, outputChannel); const provider = new GitSCMProvider(model, commandCenter); const contentProvider = new GitContentProvider(git, rootPath, onGitChange); const checkoutStatusBar = new CheckoutStatusBar(model); @@ -50,6 +52,7 @@ async function init(disposables: Disposable[]): Promise { const mergeDecorator = new MergeDecorator(model); disposables.push( + commitHandler, commandCenter, provider, contentProvider, diff --git a/extensions/git/src/scmProvider.ts b/extensions/git/src/scmProvider.ts index 925f7f0ef3e..f06078c31bc 100644 --- a/extensions/git/src/scmProvider.ts +++ b/extensions/git/src/scmProvider.ts @@ -21,10 +21,6 @@ export class GitSCMProvider implements SCMProvider { scm.registerSCMProvider('git', this); } - commit(message: string): Thenable { - return this.commandCenter.commit(message); - } - open(resource: Resource): ProviderResult { return this.commandCenter.open(resource); } diff --git a/src/vs/editor/contrib/hover/browser/hover.ts b/src/vs/editor/contrib/hover/browser/hover.ts index b2be03752c8..dc3e994478c 100644 --- a/src/vs/editor/contrib/hover/browser/hover.ts +++ b/src/vs/editor/contrib/hover/browser/hover.ts @@ -24,7 +24,7 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import EditorContextKeys = editorCommon.EditorContextKeys; @editorContribution -class ModesHoverController implements editorCommon.IEditorContribution { +export class ModesHoverController implements editorCommon.IEditorContribution { private static ID = 'editor.contrib.hover'; diff --git a/src/vs/workbench/electron-browser/workbench.main.ts b/src/vs/workbench/electron-browser/workbench.main.ts index d5e22d521d2..65e39ca0afe 100644 --- a/src/vs/workbench/electron-browser/workbench.main.ts +++ b/src/vs/workbench/electron-browser/workbench.main.ts @@ -43,8 +43,8 @@ import 'vs/workbench/parts/search/browser/search.contribution'; import 'vs/workbench/parts/search/browser/searchViewlet'; // can be packaged separately import 'vs/workbench/parts/search/browser/openAnythingHandler'; // can be packaged separately -import 'vs/workbench/parts/scm/browser/scm.contribution'; -import 'vs/workbench/parts/scm/browser/scmViewlet'; // can be packaged separately +import 'vs/workbench/parts/scm/electron-browser/scm.contribution'; +import 'vs/workbench/parts/scm/electron-browser/scmViewlet'; // can be packaged separately import 'vs/workbench/parts/git/electron-browser/git.contribution'; import 'vs/workbench/parts/git/browser/gitQuickOpen'; diff --git a/src/vs/workbench/parts/scm/browser/dirtydiffDecorator.ts b/src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts similarity index 100% rename from src/vs/workbench/parts/scm/browser/dirtydiffDecorator.ts rename to src/vs/workbench/parts/scm/electron-browser/dirtydiffDecorator.ts diff --git a/src/vs/workbench/parts/scm/browser/media/check-inverse.svg b/src/vs/workbench/parts/scm/electron-browser/media/check-inverse.svg similarity index 100% rename from src/vs/workbench/parts/scm/browser/media/check-inverse.svg rename to src/vs/workbench/parts/scm/electron-browser/media/check-inverse.svg diff --git a/src/vs/workbench/parts/scm/browser/media/check.svg b/src/vs/workbench/parts/scm/electron-browser/media/check.svg similarity index 100% rename from src/vs/workbench/parts/scm/browser/media/check.svg rename to src/vs/workbench/parts/scm/electron-browser/media/check.svg diff --git a/src/vs/workbench/parts/scm/browser/media/diff.svg b/src/vs/workbench/parts/scm/electron-browser/media/diff.svg similarity index 100% rename from src/vs/workbench/parts/scm/browser/media/diff.svg rename to src/vs/workbench/parts/scm/electron-browser/media/diff.svg diff --git a/src/vs/workbench/parts/scm/browser/media/dirtydiffDecorator.css b/src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css similarity index 100% rename from src/vs/workbench/parts/scm/browser/media/dirtydiffDecorator.css rename to src/vs/workbench/parts/scm/electron-browser/media/dirtydiffDecorator.css diff --git a/src/vs/workbench/parts/scm/browser/media/scmViewlet.css b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css similarity index 89% rename from src/vs/workbench/parts/scm/browser/media/scmViewlet.css rename to src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css index cdef1d74c0b..8f2b7fcd0e3 100644 --- a/src/vs/workbench/parts/scm/browser/media/scmViewlet.css +++ b/src/vs/workbench/parts/scm/electron-browser/media/scmViewlet.css @@ -9,18 +9,10 @@ background-position: center; } -.scm-viewlet > .scm-commit-box { +.scm-viewlet > .scm-editor { padding: 5px 9px 5px 16px; } -.scm-viewlet > .scm-commit-box > .monaco-inputbox > .wrapper > .mirror { - max-height: 134px; -} - -.scm-viewlet > .scm-commit-box.scroll > .monaco-inputbox > .wrapper > textarea.input { - overflow-y: scroll; -} - .scm-viewlet .monaco-list-row { padding: 0 12px 0 20px; line-height: 22px; diff --git a/src/vs/workbench/parts/scm/browser/scm.contribution.ts b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts similarity index 97% rename from src/vs/workbench/parts/scm/browser/scm.contribution.ts rename to src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts index 959d1d1dfbe..ab6b871eb79 100644 --- a/src/vs/workbench/parts/scm/browser/scm.contribution.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scm.contribution.ts @@ -21,7 +21,7 @@ import { ISCMService } from 'vs/workbench/services/scm/common/scm'; import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StatusUpdater } from './scmActivity'; -import SCMPreview, { DisableSCMPreviewAction, EnableSCMPreviewAction } from 'vs/workbench/parts/scm/browser/scmPreview'; +import SCMPreview, { DisableSCMPreviewAction, EnableSCMPreviewAction } from '../browser/scmPreview'; class OpenSCMViewletAction extends ToggleViewletAction { @@ -64,7 +64,7 @@ Registry.as(WorkbenchExtensions.Workbench) if (SCMPreview.enabled) { const viewletDescriptor = new ViewletDescriptor( - 'vs/workbench/parts/scm/browser/scmViewlet', + 'vs/workbench/parts/scm/electron-browser/scmViewlet', 'SCMViewlet', VIEWLET_ID, localize('scm', "SCM"), diff --git a/src/vs/workbench/parts/scm/browser/scmActivity.ts b/src/vs/workbench/parts/scm/electron-browser/scmActivity.ts similarity index 100% rename from src/vs/workbench/parts/scm/browser/scmActivity.ts rename to src/vs/workbench/parts/scm/electron-browser/scmActivity.ts diff --git a/src/vs/workbench/parts/scm/electron-browser/scmEditor.ts b/src/vs/workbench/parts/scm/electron-browser/scmEditor.ts new file mode 100644 index 00000000000..a2fb66de18f --- /dev/null +++ b/src/vs/workbench/parts/scm/electron-browser/scmEditor.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IModel, IEditorOptions, IDimension } from 'vs/editor/common/editorCommon'; +import { memoize } from 'vs/base/common/decorators'; +import { EditorAction, CommonEditorRegistry } from 'vs/editor/common/editorCommonExtensions'; +import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; +import { IEditorContributionCtor } from 'vs/editor/browser/editorBrowser'; +import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget'; +import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ICommandService } from 'vs/platform/commands/common/commands'; +import { MenuPreventer } from 'vs/editor/contrib/multicursor/browser/menuPreventer'; +import { SelectionClipboard } from 'vs/editor/contrib/selectionClipboard/electron-browser/selectionClipboard'; +import { ContextMenuController } from 'vs/editor/contrib/contextmenu/browser/contextmenu'; +import { SuggestController } from 'vs/editor/contrib/suggest/browser/suggestController'; +import { SnippetController } from 'vs/editor/contrib/snippet/common/snippetController'; +import { TabCompletionController } from 'vs/editor/contrib/suggest/browser/tabCompletion'; +import { ModesHoverController } from 'vs/editor/contrib/hover/browser/hover'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { IThemeService } from 'vs/workbench/services/themes/common/themeService'; +import URI from 'vs/base/common/uri'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import Event, { Emitter } from 'vs/base/common/event'; +import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; +import { ITextModelResolverService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; + +class SCMCodeEditorWidget extends CodeEditorWidget { + + constructor( + domElement: HTMLElement, + options: IEditorOptions, + @IInstantiationService instantiationService: IInstantiationService, + @ICodeEditorService codeEditorService: ICodeEditorService, + @ICommandService commandService: ICommandService, + @IContextKeyService contextKeyService: IContextKeyService + ) { + super(domElement, options, instantiationService, codeEditorService, commandService, contextKeyService); + } + + protected _getContributions(): IEditorContributionCtor[] { + return [ + MenuPreventer, + SelectionClipboard, + ContextMenuController, + SuggestController, + SnippetController, + TabCompletionController, + ModesHoverController + ]; + } + + protected _getActions(): EditorAction[] { + return CommonEditorRegistry.getEditorActions(); + } +} + +export const InSCMInputContextKey = new RawContextKey('inSCMInput', false); + +export class SCMEditor implements ITextModelContentProvider { + + private editor: SCMCodeEditorWidget; + private model: IModel; + private disposables: IDisposable[] = []; + + @memoize + get onDidChangeContent(): Event { + let listener: IDisposable; + const emitter = new Emitter({ + onFirstListenerAdd: () => listener = this.model.onDidChangeContent(() => emitter.fire()), + onLastListenerRemove: () => dispose(listener) + }); + + return emitter.event; + } + + private get editorOptions(): IEditorOptions { + return { + wrappingColumn: 0, + overviewRulerLanes: 0, + glyphMargin: false, + lineNumbers: 'off', + folding: false, + selectOnLineNumbers: false, + selectionHighlight: false, + scrollbar: { + horizontal: 'hidden' + }, + lineDecorationsWidth: 0, + scrollBeyondLastLine: false, + theme: this.themeService.getColorTheme().id, + renderLineHighlight: 'none', + fixedOverflowWidgets: true, + acceptSuggestionOnEnter: false, + wordWrap: true + }; + } + + constructor( + container: HTMLElement, + @IThemeService private themeService: IThemeService, + @IInstantiationService instantiationService: IInstantiationService, + @IModelService private modelService: IModelService, + @IContextKeyService private contextKeyService: IContextKeyService, + @ITextModelResolverService private textModelResolverService: ITextModelResolverService + ) { + textModelResolverService.registerTextModelContentProvider('scm', this); + + const scopedContextKeyService = this.contextKeyService.createScoped(container); + InSCMInputContextKey.bindTo(scopedContextKeyService).set(true); + this.disposables.push(scopedContextKeyService); + + const services = new ServiceCollection(); + services.set(IContextKeyService, scopedContextKeyService); + const scopedInstantiationService = instantiationService.createChild(services); + + this.editor = scopedInstantiationService.createInstance(SCMCodeEditorWidget, container, this.editorOptions); + this.themeService.onDidColorThemeChange(e => this.editor.updateOptions(this.editorOptions), null, this.disposables); + + textModelResolverService.createModelReference(URI.parse('scm:input')).done(ref => { + this.model = ref.object.textEditorModel; + this.editor.setModel(this.model); + }); + } + + get lineHeight(): number { + return this.editor.getConfiguration().lineHeight; + } + + // TODO@joao TODO@alex isn't there a better way to get the number of lines? + get lineCount(): number { + if (!this.model) { + return 0; + } + + const modelLength = this.model.getValueLength(); + const lastPosition = this.model.getPositionAt(modelLength); + const lastLineTop = this.editor.getTopForPosition(lastPosition.lineNumber, lastPosition.column); + const viewHeight = lastLineTop + this.lineHeight; + + return viewHeight / this.lineHeight; + } + + layout(dimension: IDimension): void { + this.editor.layout(dimension); + } + + focus(): void { + this.editor.focus(); + } + + provideTextContent(resource: URI): TPromise { + if (resource.toString() !== 'scm:input') { + return TPromise.as(null); + } + + return TPromise.as(this.modelService.createModel('', null, resource)); + } + + dispose(): void { + this.disposables = dispose(this.disposables); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/scm/browser/scmMenus.ts b/src/vs/workbench/parts/scm/electron-browser/scmMenus.ts similarity index 100% rename from src/vs/workbench/parts/scm/browser/scmMenus.ts rename to src/vs/workbench/parts/scm/electron-browser/scmMenus.ts diff --git a/src/vs/workbench/parts/scm/browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts similarity index 73% rename from src/vs/workbench/parts/scm/browser/scmViewlet.ts rename to src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 1008d1152b7..af019e77619 100644 --- a/src/vs/workbench/parts/scm/browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -6,12 +6,8 @@ 'use strict'; import 'vs/css!./media/scmViewlet'; -import { localize } from 'vs/nls'; -import * as platform from 'vs/base/common/platform'; import { TPromise } from 'vs/base/common/winjs.base'; import { chain } from 'vs/base/common/event'; -import { Throttler } from 'vs/base/common/async'; -import { domEvent } from 'vs/base/browser/event'; import { IDisposable, dispose, empty as EmptyDisposable } from 'vs/base/common/lifecycle'; import { Builder, Dimension } from 'vs/base/browser/builder'; import { Viewlet } from 'vs/workbench/browser/viewlet'; @@ -27,17 +23,16 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IContextViewService, IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { IMessageService, Severity } from 'vs/platform/message/common/message'; -import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; -import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; +import { IMessageService } from 'vs/platform/message/common/message'; import { IMenuService } from 'vs/platform/actions/common/actions'; -import { Action, IAction, IActionItem } from 'vs/base/common/actions'; +import { IAction, IActionItem } from 'vs/base/common/actions'; import { createActionItem } from 'vs/platform/actions/browser/menuItemActionItem'; import { SCMMenus } from './scmMenus'; import { ActionBar, IActionItemProvider } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService } from 'vs/workbench/services/themes/common/themeService'; import { isDarkTheme } from 'vs/platform/theme/common/themes'; +import { SCMEditor } from './scmEditor'; +import { IModelService } from 'vs/editor/common/services/modelService'; interface SearchInputEvent extends Event { target: HTMLInputElement; @@ -145,59 +140,10 @@ class Delegate implements IDelegate { } } -/** - * HACK - */ -class CommitAction extends Action { - - private activeProvider: ISCMProvider; - private isRunning = false; - private throttler = new Throttler(); - private disposables: IDisposable[] = []; - - constructor( - private inputBox: InputBox, - @ISCMService scmService: ISCMService, - @IMessageService private messageService: IMessageService - ) { - super('scm.commit', localize('commit', "Commit"), 'scm-commit'); - - this.setActiveProvider(scmService.activeProvider); - scmService.onDidChangeProvider(this.setActiveProvider, this, this.disposables); - inputBox.onDidChange(this.updateEnablement, this, this.disposables); - } - - private setActiveProvider(activeProvider: ISCMProvider | undefined): void { - this.activeProvider = activeProvider; - this.updateEnablement(); - } - - private updateEnablement(): void { - this.enabled = !!this.activeProvider && !this.isRunning && !!this.inputBox.value; - } - - run(): TPromise { - return this.throttler - .queue(() => { - this.isRunning = true; - return this.activeProvider.commit(this.inputBox.value); - }) - .then(() => this.inputBox.value = '', err => this.messageService.show(Severity.Error, err)) - .then(() => this.isRunning = false); - } - - dispose(): void { - this.disposables = dispose(this.disposables); - } -} - export class SCMViewlet extends Viewlet { - private static ACCEPT_KEYBINDING = platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter'; - private cachedDimension: Dimension; - private inputBoxContainer: HTMLElement; - private inputBox: InputBox; + private editor: SCMEditor; private listContainer: HTMLElement; private list: List; private menus: SCMMenus; @@ -214,7 +160,8 @@ export class SCMViewlet extends Viewlet { @IMessageService private messageService: IMessageService, @IContextMenuService private contextMenuService: IContextMenuService, @IThemeService private themeService: IThemeService, - @IMenuService private menuService: IMenuService + @IMenuService private menuService: IMenuService, + @IModelService private modelService: IModelService ) { super(VIEWLET_ID, telemetryService); @@ -240,22 +187,22 @@ export class SCMViewlet extends Viewlet { parent.addClass('scm-viewlet'); const root = parent.getHTMLElement(); - this.inputBoxContainer = append(root, $('.scm-commit-box')); + const editorContainer = append(root, $('.scm-editor')); - this.inputBox = new InputBox(this.inputBoxContainer, this.contextViewService, { - placeholder: localize('accept', "Message (press {0} to submit)", SCMViewlet.ACCEPT_KEYBINDING), - ariaLabel: localize('acceptAria', "Changes: Type message and press {0} to accept the changes", SCMViewlet.ACCEPT_KEYBINDING), - flexibleHeight: true - }); + this.editor = this.instantiationService.createInstance(SCMEditor, editorContainer); + this.editor.onDidChangeContent(() => this.layout(), null, this.disposables); + this.disposables.push(this.editor); - chain(domEvent(this.inputBox.inputElement, 'keydown')) - .map(e => new StandardKeyboardEvent(e)) - .filter(e => e.equals(KeyMod.CtrlCmd | KeyCode.Enter) || e.equals(KeyMod.CtrlCmd | KeyCode.KEY_S)) - .on(this.accept, this, this.disposables); + // this.inputBox = new InputBox(this.inputBoxContainer, this.contextViewService, { + // placeholder: localize('accept', "Message (press {0} to submit)", SCMViewlet.ACCEPT_KEYBINDING), + // ariaLabel: localize('acceptAria', "Changes: Type message and press {0} to accept the changes", SCMViewlet.ACCEPT_KEYBINDING), + // flexibleHeight: true + // }); - chain(this.inputBox.onDidHeightChange) - .map(() => this.cachedDimension) - .on(this.layout, this, this.disposables); + // chain(domEvent(this.inputBox.inputElement, 'keydown')) + // .map(e => new StandardKeyboardEvent(e)) + // .filter(e => e.equals(KeyMod.CtrlCmd | KeyCode.Enter) || e.equals(KeyMod.CtrlCmd | KeyCode.KEY_S)) + // .on(this.accept, this, this.disposables); this.listContainer = append(root, $('.scm-status.show-file-icons')); const delegate = new Delegate(); @@ -273,7 +220,7 @@ export class SCMViewlet extends Viewlet { .on(this.open, this, this.disposables); this.list.onContextMenu(this.onListContextMenu, this, this.disposables); - this.disposables.push(this.inputBox, this.list); + this.disposables.push(this.list); this.setActiveProvider(this.scmService.activeProvider); this.scmService.onDidChangeProvider(this.setActiveProvider, this, this.disposables); @@ -290,7 +237,6 @@ export class SCMViewlet extends Viewlet { return; } - const elements = provider.resources .reduce<(ISCMResourceGroup | ISCMResource)[]>((r, g) => [...r, g, ...g.resources], []); @@ -303,13 +249,13 @@ export class SCMViewlet extends Viewlet { } this.cachedDimension = dimension; - this.inputBox.layout(); - const listHeight = dimension.height - (this.inputBox.height + 12 /* margin */); + const editorHeight = Math.min(this.editor.lineCount, 8) * this.editor.lineHeight; + this.editor.layout({ width: dimension.width - 25, height: editorHeight }); + + const listHeight = dimension.height - (editorHeight + 12 /* margin */); this.listContainer.style.height = `${listHeight}px`; this.list.layout(listHeight); - - toggleClass(this.inputBoxContainer, 'scroll', this.inputBox.height >= 134); } getOptimalWidth(): number { @@ -318,14 +264,7 @@ export class SCMViewlet extends Viewlet { focus(): void { super.focus(); - this.inputBox.focus(); - } - - private acceptThrottler = new Throttler(); - private accept(): void { - this.acceptThrottler - .queue(() => this.scmService.activeProvider.commit(this.inputBox.value)) - .done(() => this.inputBox.value = '', err => this.messageService.show(Severity.Error, err)); + this.editor.focus(); } private open(e: ISCMResource): void { @@ -333,10 +272,7 @@ export class SCMViewlet extends Viewlet { } getActions(): IAction[] { - return [ - this.instantiationService.createInstance(CommitAction, this.inputBox), - ...this.menus.getTitleActions() - ]; + return this.menus.getTitleActions(); } getSecondaryActions(): IAction[] {