Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notebooks respect files.xyz settings #192941

Merged
merged 6 commits into from
Sep 18, 2023
Merged
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,38 @@
import { localize } from 'vs/nls';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { IBulkEditService, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { isEqual } from 'vs/base/common/resources';
import * as strings from 'vs/base/common/strings';
import { IBulkEditService, ResourceEdit, ResourceTextEdit } from 'vs/editor/browser/services/bulkEditService';
import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { CodeActionProvider, CodeActionTriggerType, IWorkspaceTextEdit } from 'vs/editor/common/languages';
import { ITextModel } from 'vs/editor/common/model';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { ApplyCodeActionReason, applyCodeAction, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction';
import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types';
import { getDocumentFormattingEditsUntilResult } from 'vs/editor/contrib/format/browser/format';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ILogService } from 'vs/platform/log/common/log';
import { IProgress, IProgressStep } from 'vs/platform/progress/common/progress';
import { Registry } from 'vs/platform/registry/common/platform';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchContributionsExtensions } from 'vs/workbench/common/contributions';
import { SaveReason } from 'vs/workbench/common/editor';
import { ICellViewModel, getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { NotebookFileWorkingCopyModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy';
import { IStoredFileWorkingCopySaveParticipant, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
import { ITextModel } from 'vs/editor/common/model';
import { ILogService } from 'vs/platform/log/common/log';
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types';
import { CodeActionTriggerType, CodeActionProvider, IWorkspaceTextEdit } from 'vs/editor/common/languages';
import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction';
import { isEqual } from 'vs/base/common/resources';

const NotebookCodeAction = new CodeActionKind('notebook');


class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant {
constructor(
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
Expand Down Expand Up @@ -86,7 +91,217 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant {
return [];
}));

await this.bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('label', "Format Notebook"), code: 'undoredo.formatNotebook', });
await this.bulkEditService.apply(/* edit */allCellEdits.flat(), { label: localize('formatNotebook', "Format Notebook"), code: 'undoredo.formatNotebook', });

} finally {
progress.report({ increment: 100 });
disposable.dispose();
}
}
}

class TrimWhitespaceParticipant implements IStoredFileWorkingCopySaveParticipant {

constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@ITextModelService private readonly textModelService: ITextModelService,
@IBulkEditService private readonly bulkEditService: IBulkEditService,
) { }

async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
if (this.configurationService.getValue<boolean>('files.trimTrailingWhitespace')) {
await this.doTrimTrailingWhitespace(workingCopy, context, progress);
}
}

private async doTrimTrailingWhitespace(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>) {
if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) {
return;
}

const disposable = new DisposableStore();
const notebook = workingCopy.model.notebookModel;

const cursors: Position[] = [];
let viewCell: ICellViewModel | undefined = undefined;

// autosave -- don't trim entire line, only up to cursor, so need to track position of cursor(s)
if (context.reason === SaveReason.AUTO) {
viewCell = getNotebookViewCell(this.editorService);
if (!viewCell) {
return;
}
const selections = viewCell.getSelections();
for (const sel of selections) {
if (viewCell.model.textModel) {
cursors.push(new Position(sel.selectionStartLineNumber, sel.startColumn));
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
}
}

}

try {
const allCellEdits = await Promise.all(notebook.cells.map(async (cell) => {
if (cell.cellKind !== 2) {
return [];
}

const ref = await this.textModelService.createModelReference(cell.uri);
disposable.add(ref);
const model = ref.object.textEditorModel;
const ops = trimTrailingWhitespace(model, (viewCell && viewCell.model.textModel === model) ? cursors : []);
if (!ops.length) {
return []; // Nothing to do
}

return ops.map(op => new ResourceTextEdit(model.uri, { ...op, text: op.text || '' }, model.getVersionId()));
}));

const filteredEdits = allCellEdits.flat().filter(edit => edit !== undefined) as ResourceEdit[];
await this.bulkEditService.apply(filteredEdits, { label: localize('trimNotebookWhitespace', "Notebook Trim Trailing Whitespace"), code: 'undoredo.notebookTrimTrailingWhitespace' });

} finally {
progress.report({ increment: 100 });
disposable.dispose();
}
}
}

class TrimFinalNewLinesParticipant implements IStoredFileWorkingCopySaveParticipant {

constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@ITextModelService private readonly textModelService: ITextModelService,
@IBulkEditService private readonly bulkEditService: IBulkEditService,
) { }

async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
if (this.configurationService.getValue<boolean>('files.trimTrailingWhitespace')) {
this.doTrimFinalNewLines(workingCopy, context, progress);
}
}

/**
* returns 0 if the entire file is empty
*/
private findLastNonEmptyLine(model: ITextModel): number {
for (let lineNumber = model.getLineCount(); lineNumber >= 1; lineNumber--) {
const lineContent = model.getLineContent(lineNumber);
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
if (lineContent.length > 0) {
// this line has content
return lineNumber;
}
}
// no line has content
return 0;
}

private async doTrimFinalNewLines(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>): Promise<void> {
if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) {
return;
}

const disposable = new DisposableStore();
const notebook = workingCopy.model.notebookModel;

let cannotTouchLineNumber = 0;
let viewCell: ICellViewModel | undefined = undefined;

// autosave -- don't trim entire line, only up to cursor, so need to track position of cursor(s)
if (context.reason === SaveReason.AUTO) {
viewCell = getNotebookViewCell(this.editorService);
if (!viewCell) {
return;
}
const selections = viewCell.getSelections();
for (const sel of selections) {
if (viewCell.model.textModel) {
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
cannotTouchLineNumber = Math.max(cannotTouchLineNumber, sel.selectionStartLineNumber);
}
}
}


try {
const allCellEdits = await Promise.all(notebook.cells.map(async (cell) => {
if (cell.cellKind !== 2) {
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
return;
}

const ref = await this.textModelService.createModelReference(cell.uri);
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
disposable.add(ref);
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
const model = ref.object.textEditorModel;

const lastNonEmptyLine = this.findLastNonEmptyLine(model);
const deleteFromLineNumber = Math.max(lastNonEmptyLine + 1, cannotTouchLineNumber + 1);
const deletionRange = model.validateRange(new Range(deleteFromLineNumber, 1, model.getLineCount(), model.getLineMaxColumn(model.getLineCount())));

if (deletionRange.isEmpty()) {
return;
}

// create the edit to delete all lines in deletionRange
return new ResourceTextEdit(model.uri, { range: deletionRange, text: '' }, model.getVersionId());
}));

const filteredEdits = allCellEdits.flat().filter(edit => edit !== undefined) as ResourceEdit[];
await this.bulkEditService.apply(filteredEdits, { label: localize('trimNotebookNewlines', "Trim Final New Lines"), code: 'undoredo.trimFinalNewLines' });

} finally {
progress.report({ increment: 100 });
disposable.dispose();
}
}
}

class FinalNewLineParticipant implements IStoredFileWorkingCopySaveParticipant {
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved

constructor(
@IConfigurationService private readonly configurationService: IConfigurationService,
// @IEditorService private readonly editorService: IEditorService,
@ITextModelService private readonly textModelService: ITextModelService,
@IBulkEditService private readonly bulkEditService: IBulkEditService,
) { }

async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
if (this.configurationService.getValue('files.insertFinalNewline')) {
this.doInsertFinalNewLine(workingCopy, context, progress);
}
}

private async doInsertFinalNewLine(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>): Promise<void> {
if (!workingCopy.model || !(workingCopy.model instanceof NotebookFileWorkingCopyModel)) {
return;
}

const disposable = new DisposableStore();
const notebook = workingCopy.model.notebookModel;

try {
const allCellEdits = await Promise.all(notebook.cells.map(async (cell) => {
if (cell.cellKind !== 2) {
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
return;
}

const ref = await this.textModelService.createModelReference(cell.uri);
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
disposable.add(ref);
const model = ref.object.textEditorModel;

const lineCount = model.getLineCount();
const lastLine = model.getLineContent(lineCount);
const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1;

Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
if (!lineCount || lastLineIsEmptyOrWhitespace) {
return;
}

return new ResourceTextEdit(model.uri, { range: new Range(lineCount, model.getLineMaxColumn(lineCount), lineCount, model.getLineMaxColumn(lineCount)), text: model.getEOL() }, model.getVersionId());
}));

const filteredEdits = allCellEdits.flat().filter(edit => edit !== undefined) as ResourceEdit[];
await this.bulkEditService.apply(filteredEdits, { label: localize('insertFinalNewLine', "Insert Final New Line"), code: 'undoredo.insertFinalNewLine' });

} finally {
progress.report({ increment: 100 });
Expand Down Expand Up @@ -287,7 +502,16 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa
}
}


function getNotebookViewCell(editorService: IEditorService): ICellViewModel | undefined {
const activePane = editorService.activeEditorPane;
const notebookEditor = getNotebookEditorFromEditorPane(activePane);
const notebookViewModel = notebookEditor?.getViewModel();
const cellSelections = notebookViewModel?.getSelections();
if (!cellSelections || !notebookViewModel || !notebookEditor?.textModel) {
return;
}
return notebookViewModel.viewCells[cellSelections[0].start];
}

export class SaveParticipantsContribution extends Disposable implements IWorkbenchContribution {
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
constructor(
Expand All @@ -299,8 +523,12 @@ export class SaveParticipantsContribution extends Disposable implements IWorkben
}

private registerSaveParticipants(): void {
this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(TrimWhitespaceParticipant)));
this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(CodeActionOnSaveParticipant)));
this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(FormatOnSaveParticipant)));
this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(FinalNewLineParticipant)));
this._register(this.workingCopyFileService.addSaveParticipant(this.instantiationService.createInstance(TrimFinalNewLinesParticipant)));

}
}
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved

Expand Down
Loading