Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,53 @@ export class FinalNewLineParticipant implements INamedSaveParticpant {
}
}

export class TrimFinalNewLinesParticipant implements INamedSaveParticpant {

readonly name = 'TrimFinalNewLinesParticipant';

constructor(
@IConfigurationService private configurationService: IConfigurationService,
@ICodeEditorService private codeEditorService: ICodeEditorService
) {
// Nothing
}

public participate(model: ITextFileEditorModel, env: { reason: SaveReason }): void {
if (this.configurationService.lookup('files.trimFinalNewlines', { overrideIdentifier: model.textEditorModel.getLanguageIdentifier().language, resource: model.getResource() }).value) {
this.doTrimFinalNewLines(model.textEditorModel);
}
}

private doTrimFinalNewLines(model: IModel): void {
const lineCount = model.getLineCount();

// Do not insert new line if file does not end with new line
if (!lineCount) {
return;
}

let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)];
const editor = findEditor(model, this.codeEditorService);
if (editor) {
prevSelection = editor.getSelections();
}

let currentLineNumber = model.getLineCount();
let currentLine = model.getLineContent(currentLineNumber);
let currentLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(currentLine) === -1;
while (currentLineIsEmptyOrWhitespace) {
currentLineNumber--;
currentLine = model.getLineContent(currentLineNumber);
currentLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(currentLine) === -1;
}
model.pushEditOperations(prevSelection, [EditOperation.delete(new Range(currentLineNumber + 1, 1, lineCount + 1, 1))], edits => prevSelection);

if (editor) {
editor.setSelections(prevSelection);
}
}
}

class FormatOnSaveParticipant implements INamedSaveParticpant {

readonly name = 'FormatOnSaveParticipant';
Expand Down Expand Up @@ -237,6 +284,7 @@ export class SaveParticipant implements ISaveParticipant {
new TrimWhitespaceParticipant(configurationService, codeEditorService),
new FormatOnSaveParticipant(codeEditorService, configurationService),
new FinalNewLineParticipant(configurationService, codeEditorService),
new TrimFinalNewLinesParticipant(configurationService, codeEditorService),
new ExtHostSaveParticipant(extHostContext)
];

Expand Down
7 changes: 7 additions & 0 deletions src/vs/workbench/parts/files/browser/files.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,13 @@ configurationRegistry.registerConfiguration({
'overridable': true,
'scope': ConfigurationScope.RESOURCE
},
'files.trimFinalNewlines': {
'type': 'boolean',
'default': false,
'description': nls.localize('trimFinalNewlines', "When enabled, will trim all new lines after the final new line at the end of the file when saving it."),
'overridable': true,
'scope': ConfigurationScope.RESOURCE
},
'files.autoSave': {
'type': 'string',
'enum': [AutoSaveConfiguration.OFF, AutoSaveConfiguration.AFTER_DELAY, AutoSaveConfiguration.ON_FOCUS_CHANGE, , AutoSaveConfiguration.ON_WINDOW_CHANGE],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import * as assert from 'assert';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { FinalNewLineParticipant } from 'vs/workbench/api/electron-browser/mainThreadSaveParticipant';
import { FinalNewLineParticipant, TrimFinalNewLinesParticipant } from 'vs/workbench/api/electron-browser/mainThreadSaveParticipant';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { workbenchInstantiationService, TestTextFileService } from 'vs/workbench/test/workbenchTestServices';
import { toResource } from 'vs/base/test/common/utils';
Expand Down Expand Up @@ -72,4 +72,44 @@ suite('MainThreadSaveParticipant', function () {
done();
});
});

test('trim final new lines', function (done) {
const model: TextFileEditorModel = instantiationService.createInstance(TextFileEditorModel, toResource.call(this, '/path/trim_final_new_line.txt'), 'utf8');

model.load().then(() => {
const configService = new TestConfigurationService();
configService.setUserConfiguration('files', { 'trimFinalNewlines': true });

const participant = new TrimFinalNewLinesParticipant(configService, undefined);

const textContent = 'Trim New Line';
const eol = `${model.textEditorModel.getEOL()}`;

// No new line removal if last line is not new line
let lineContent = `${textContent}`;
model.textEditorModel.setValue(lineContent);
participant.participate(model, { reason: SaveReason.EXPLICIT });
assert.equal(model.getValue(), lineContent);

// No new line removal if last line is single new line
lineContent = `${textContent}${eol}`;
model.textEditorModel.setValue(lineContent);
participant.participate(model, { reason: SaveReason.EXPLICIT });
assert.equal(model.getValue(), lineContent);

// Remove new line (single line with two new lines)
lineContent = `${textContent}${eol}${eol}`;
model.textEditorModel.setValue(lineContent);
participant.participate(model, { reason: SaveReason.EXPLICIT });
assert.equal(model.getValue(), `${textContent}${eol}`);

// Remove new lines (multiple lines with multiple new lines)
lineContent = `${textContent}${eol}${textContent}${eol}${eol}${eol}`;
model.textEditorModel.setValue(lineContent);
participant.participate(model, { reason: SaveReason.EXPLICIT });
assert.equal(model.getValue(), `${textContent}${eol}${textContent}${eol}`);

done();
});
});
});