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 4 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,40 @@
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 { ICodeEditor } from 'vs/editor/browser/editorBrowser';
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 { Selection } from 'vs/editor/common/core/selection';
import { CodeActionProvider, CodeActionTriggerType, IWorkspaceTextEdit } from 'vs/editor/common/languages';
import { IReadonlyTextBuffer, 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 { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
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 { getNotebookEditorFromEditorPane } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
import { CellKind, 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 +93,207 @@ 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.reason === SaveReason.AUTO, progress);
}
}

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

const disposable = new DisposableStore();
const notebook = workingCopy.model.notebookModel;
const activeCellEditor = getActiveCellCodeEditor(this.editorService);

let cursors: Position[] = [];
let prevSelection: Selection[] = [];
try {
const allCellEdits = await Promise.all(notebook.cells.map(async (cell) => {
if (cell.cellKind !== CellKind.Code) {
return [];
}

const ref = await this.textModelService.createModelReference(cell.uri);
disposable.add(ref);
const model = ref.object.textEditorModel;

const isActiveCell = (activeCellEditor && cell.uri.toString() === activeCellEditor.getModel()?.uri.toString());
if (isActiveCell) {
prevSelection = activeCellEditor.getSelections() ?? [];
if (isAutoSaved) {
cursors = prevSelection.map(s => s.getPosition()); // get initial cursor positions
const snippetsRange = SnippetController2.get(activeCellEditor)?.getSessionEnclosingRange();
if (snippetsRange) {
for (let lineNumber = snippetsRange.startLineNumber; lineNumber <= snippetsRange.endLineNumber; lineNumber++) {
cursors.push(new Position(lineNumber, model.getLineMaxColumn(lineNumber)));
}
}
}
}

const ops = trimTrailingWhitespace(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.reason === SaveReason.AUTO, progress);
}
}

/**
* returns 0 if the entire file is empty
*/
private findLastNonEmptyLine(textBuffer: IReadonlyTextBuffer): number {
for (let lineNumber = textBuffer.getLineCount(); lineNumber >= 1; lineNumber--) {
const lineLength = textBuffer.getLineLength(lineNumber);
if (lineLength) {
// this line has content
return lineNumber;
}
}
// no line has content
return 0;
}

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

const disposable = new DisposableStore();
const notebook = workingCopy.model.notebookModel;
const activeCellEditor = getActiveCellCodeEditor(this.editorService);

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;
}

// autosave -- don't trim every trailing line, just up to the cursor line
let cannotTouchLineNumber = 0;
const isActiveCell = (activeCellEditor && cell.uri.toString() === activeCellEditor.getModel()?.uri.toString());
if (isAutoSaved && isActiveCell) {
const selections = activeCellEditor.getSelections() ?? [];
for (const sel of selections) {
cannotTouchLineNumber = Math.max(cannotTouchLineNumber, sel.selectionStartLineNumber);
}
}

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 textBuffer = cell.textBuffer;

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

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

// create the edit to delete all lines in deletionRange
return new ResourceTextEdit(cell.uri, { range: deletionRange, text: '' }, cell.textModel?.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,
@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 lineCount = cell.textBuffer.getLineCount();
const lastLineIsEmptyOrWhitespace = cell.textBuffer.getLineFirstNonWhitespaceColumn(lineCount) === 0;

if (!lineCount || lastLineIsEmptyOrWhitespace) {
return;
}

return new ResourceTextEdit(cell.uri, { range: new Range(lineCount + 1, cell.textBuffer.getLineLength(lineCount), lineCount + 1, cell.textBuffer.getLineLength(lineCount)), text: cell.textBuffer.getEOL() }, cell.textModel?.getVersionId());
}));

const filteredEdits = allCellEdits.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 +494,12 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa
}
}


function getActiveCellCodeEditor(editorService: IEditorService): ICodeEditor | undefined {
const activePane = editorService.activeEditorPane;
const notebookEditor = getNotebookEditorFromEditorPane(activePane);
const activeCodeEditor = notebookEditor?.activeCodeEditor;
return activeCodeEditor;
}

export class SaveParticipantsContribution extends Disposable implements IWorkbenchContribution {
Yoyokrazy marked this conversation as resolved.
Show resolved Hide resolved
constructor(
Expand All @@ -299,8 +511,11 @@ 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