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 index 8ec8fd2a1e3..7010838f6f7 100644 --- a/extensions/git/src/commit.ts +++ b/extensions/git/src/commit.ts @@ -5,15 +5,16 @@ 'use strict'; -import { workspace, window, languages, Disposable, Uri, TextDocumentChangeEvent, HoverProvider, Hover, TextEditor, Position, TextDocument, Range, TextEditorDecorationType } from 'vscode'; +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() === 'scm:input'; + return uri.toString() === scmInputUri.toString(); } interface Diagnostic { @@ -22,7 +23,7 @@ interface Diagnostic { } // TODO@Joao: hover dissapears if editor is scrolled -export class CommitHandler implements HoverProvider { +export class CommitController implements HoverProvider { private visibleTextEditorsDisposable: Disposable; private editor: TextEditor; @@ -30,6 +31,28 @@ export class CommitHandler implements HoverProvider { 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); diff --git a/extensions/git/src/main.ts b/extensions/git/src/main.ts index a724582c0ed..61b145296be 100644 --- a/extensions/git/src/main.ts +++ b/extensions/git/src/main.ts @@ -15,7 +15,7 @@ import { filterEvent, anyEvent } from './util'; import { GitContentProvider } from './contentProvider'; import { AutoFetcher } from './autofetch'; import { MergeDecorator } from './merge'; -import { CommitHandler } from './commit'; +import { CommitController } from './commit'; import * as nls from 'vscode-nls'; const localize = nls.config()(); @@ -42,16 +42,17 @@ 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); const syncStatusBar = new SyncStatusBar(model); const autoFetcher = new AutoFetcher(model); const mergeDecorator = new MergeDecorator(model); - const commitHandler = new CommitHandler(model); disposables.push( + commitHandler, commandCenter, provider, contentProvider, @@ -60,8 +61,7 @@ async function init(disposables: Disposable[]): Promise { checkoutStatusBar, syncStatusBar, autoFetcher, - mergeDecorator, - commitHandler + mergeDecorator ); } 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/workbench/parts/scm/electron-browser/scmEditor.ts b/src/vs/workbench/parts/scm/electron-browser/scmEditor.ts index dda97403401..a2fb66de18f 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmEditor.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmEditor.ts @@ -5,13 +5,14 @@ '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 } from 'vs/platform/contextkey/common/contextkey'; +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'; @@ -26,6 +27,8 @@ 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 { @@ -57,7 +60,9 @@ class SCMCodeEditorWidget extends CodeEditorWidget { } } -export class SCMEditor { +export const InSCMInputContextKey = new RawContextKey('inSCMInput', false); + +export class SCMEditor implements ITextModelContentProvider { private editor: SCMCodeEditorWidget; private model: IModel; @@ -99,14 +104,28 @@ export class SCMEditor { constructor( container: HTMLElement, @IThemeService private themeService: IThemeService, - @IInstantiationService private instantiationService: IInstantiationService, - @IModelService private modelService: IModelService + @IInstantiationService instantiationService: IInstantiationService, + @IModelService private modelService: IModelService, + @IContextKeyService private contextKeyService: IContextKeyService, + @ITextModelResolverService private textModelResolverService: ITextModelResolverService ) { - this.editor = this.instantiationService.createInstance(SCMCodeEditorWidget, container, this.editorOptions); - this.model = this.modelService.createModel('', null, URI.parse(`scm:input`)); - this.editor.setModel(this.model); + 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 { @@ -115,6 +134,10 @@ export class SCMEditor { // 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); @@ -131,6 +154,14 @@ export class SCMEditor { 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); } diff --git a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts index 0befe3f94fe..b5c1a5477f4 100644 --- a/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts +++ b/src/vs/workbench/parts/scm/electron-browser/scmViewlet.ts @@ -7,7 +7,6 @@ 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'; @@ -192,8 +191,6 @@ class CommitAction extends Action { export class SCMViewlet extends Viewlet { - private static readonly ACCEPT_KEYBINDING = platform.isMacintosh ? 'Cmd+Enter' : 'Ctrl+Enter'; - private cachedDimension: Dimension; private editor: SCMEditor; private listContainer: HTMLElement; @@ -319,13 +316,6 @@ export class SCMViewlet extends Viewlet { this.editor.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)); - } - private open(e: ISCMResource): void { this.scmService.activeProvider.open(e); }