提交 ae26c5e7 编写于 作者: B Benjamin Pasero 提交者: GitHub

Merge pull request #14838 from Microsoft/ben/insertFinalNewline

Provide an option to insert a final newline (fixes #1666)
......@@ -7,6 +7,7 @@
import { TPromise } from 'vs/base/common/winjs.base';
import { sequence } from 'vs/base/common/async';
import * as strings from 'vs/base/common/strings';
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
import { IThreadService } from 'vs/workbench/services/thread/common/threadService';
import { ISaveParticipant, ITextFileEditorModel, SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
......@@ -21,8 +22,9 @@ import { EditOperationsCommand } from 'vs/editor/contrib/format/common/formatCom
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { ExtHostContext, ExtHostDocumentSaveParticipantShape } from './extHost.protocol';
import { EditOperation } from 'vs/editor/common/core/editOperation';
interface INamedSaveParticpant extends ISaveParticipant {
export interface INamedSaveParticpant extends ISaveParticipant {
readonly name: string;
}
......@@ -47,29 +49,18 @@ class TrimWhitespaceParticipant implements INamedSaveParticpant {
let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)];
const cursors: IPosition[] = [];
// Find `prevSelection` in any case do ensure a good undo stack when pushing the edit
// Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump
if (model.isAttachedToEditor()) {
const allEditors = this.codeEditorService.listCodeEditors();
for (let i = 0, len = allEditors.length; i < len; i++) {
const editor = allEditors[i];
const editorModel = editor.getModel();
if (!editorModel) {
continue; // empty editor
}
if (model === editorModel) {
prevSelection = editor.getSelections();
if (isAutoSaved) {
cursors.push(...prevSelection.map(s => {
return {
lineNumber: s.positionLineNumber,
column: s.positionColumn
};
}));
}
}
let editor = findEditor(model, this.codeEditorService);
if (editor) {
// Find `prevSelection` in any case do ensure a good undo stack when pushing the edit
// Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump
prevSelection = editor.getSelections();
if (isAutoSaved) {
cursors.push(...prevSelection.map(s => {
return {
lineNumber: s.positionLineNumber,
column: s.positionColumn
};
}));
}
}
......@@ -82,6 +73,62 @@ class TrimWhitespaceParticipant implements INamedSaveParticpant {
}
}
function findEditor(model: IModel, codeEditorService: ICodeEditorService): ICommonCodeEditor {
if (model.isAttachedToEditor()) {
const allEditors = codeEditorService.listCodeEditors();
for (let i = 0, len = allEditors.length; i < len; i++) {
const editor = allEditors[i];
const editorModel = editor.getModel();
if (!editorModel) {
continue; // empty editor
}
if (model === editorModel) {
return editor;
}
}
}
return null;
}
export class FinalNewLineParticipant implements INamedSaveParticpant {
readonly name = 'FinalNewLineParticipant';
constructor(
@IConfigurationService private configurationService: IConfigurationService,
@ICodeEditorService private codeEditorService: ICodeEditorService
) {
// Nothing
}
public participate(model: ITextFileEditorModel, env: { reason: SaveReason }): any {
if (this.configurationService.lookup('files.insertFinalNewline').value) {
this.doInsertFinalNewLine(model.textEditorModel);
}
}
private doInsertFinalNewLine(model: IModel): void {
const lineCount = model.getLineCount();
const lastLine = model.getLineContent(lineCount);
const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1;
if (!lineCount || lastLineIsEmptyOrWhitespace) {
return;
}
let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)];
const editor = findEditor(model, this.codeEditorService);
if (editor) {
prevSelection = editor.getSelections();
}
model.pushEditOperations(prevSelection, [EditOperation.insert({ lineNumber: lineCount + 1, column: 0 }, model.getEOL())], (edits) => prevSelection);
}
}
class FormatOnSaveParticipant implements INamedSaveParticpant {
readonly name = 'FormatOnSaveParticipant';
......@@ -204,6 +251,7 @@ export class SaveParticipant implements ISaveParticipant {
this._saveParticipants = [
instantiationService.createInstance(TrimWhitespaceParticipant),
instantiationService.createInstance(FinalNewLineParticipant),
instantiationService.createInstance(FormatOnSaveParticipant),
instantiationService.createInstance(ExtHostSaveParticipant)
];
......
......@@ -195,7 +195,12 @@ configurationRegistry.registerConfiguration({
'files.trimTrailingWhitespace': {
'type': 'boolean',
'default': false,
'description': nls.localize('trimTrailingWhitespace', "When enabled, will trim trailing whitespace when you save a file.")
'description': nls.localize('trimTrailingWhitespace', "When enabled, will trim trailing whitespace when saving a file.")
},
'files.insertFinalNewline': {
'type': 'boolean',
'default': false,
'description': nls.localize('insertFinalNewline', "When enabled, insert a final new line at the end of the file when saving it.")
},
'files.autoSave': {
'type': 'string',
......
/*---------------------------------------------------------------------------------------------
* 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 * as assert from 'assert';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { FinalNewLineParticipant } from 'vs/workbench/api/node/mainThreadSaveParticipant';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { workbenchInstantiationService, TestTextFileService, toResource } from 'vs/test/utils/servicesTestUtils';
import { IModelService } from 'vs/editor/common/services/modelService';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { IEventService } from 'vs/platform/event/common/event';
import { ITextFileService, SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
import { TextFileEditorModelManager } from 'vs/workbench/services/textfile/common/textFileEditorModelManager';
class ServiceAccessor {
constructor( @IEventService public eventService: IEventService, @ITextFileService public textFileService: TestTextFileService, @IModelService public modelService: IModelService) {
}
}
suite('MainThreadSaveParticipant', function () {
let instantiationService: IInstantiationService;
let accessor: ServiceAccessor;
setup(() => {
instantiationService = workbenchInstantiationService();
accessor = instantiationService.createInstance(ServiceAccessor);
});
teardown(() => {
(<TextFileEditorModelManager>accessor.textFileService.models).clear();
TextFileEditorModel.setSaveParticipant(null); // reset any set participant
});
test('insert final new line', function (done) {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/final_new_line.txt'), 'utf8');
model.load().then(() => {
const configService = new TestConfigurationService();
configService.setUserConfiguration('files', { 'insertFinalNewline': true });
const participant = new FinalNewLineParticipant(configService, undefined);
// No new line for empty lines
let lineContent = '';
model.textEditorModel.setValue(lineContent);
participant.participate(model, { reason: SaveReason.EXPLICIT });
assert.equal(model.getValue(), lineContent);
// No new line if last line already empty
lineContent = `Hello New Line${model.textEditorModel.getEOL()}`;
model.textEditorModel.setValue(lineContent);
participant.participate(model, { reason: SaveReason.EXPLICIT });
assert.equal(model.getValue(), lineContent);
// New empty line added (single line)
lineContent = 'Hello New Line';
model.textEditorModel.setValue(lineContent);
participant.participate(model, { reason: SaveReason.EXPLICIT });
assert.equal(model.getValue(), `${lineContent}${model.textEditorModel.getEOL()}`);
// New empty line added (multi line)
lineContent = `Hello New Line${model.textEditorModel.getEOL()}Hello New Line${model.textEditorModel.getEOL()}Hello New Line`;
model.textEditorModel.setValue(lineContent);
participant.participate(model, { reason: SaveReason.EXPLICIT });
assert.equal(model.getValue(), `${lineContent}${model.textEditorModel.getEOL()}`);
done();
});
});
});
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册