monaco-editor
Version:
A browser based code editor
361 lines (360 loc) • 18 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { asArray, isNonEmptyArray } from '../../../../base/common/arrays.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { onUnexpectedExternalError } from '../../../../base/common/errors.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { LinkedList } from '../../../../base/common/linkedList.js';
import { assertType } from '../../../../base/common/types.js';
import { URI } from '../../../../base/common/uri.js';
import { EditorStateCancellationTokenSource, TextModelCancellationTokenSource } from '../../editorState/browser/editorState.js';
import { isCodeEditor } from '../../../browser/editorBrowser.js';
import { Position } from '../../../common/core/position.js';
import { Range } from '../../../common/core/range.js';
import { Selection } from '../../../common/core/selection.js';
import { IEditorWorkerService } from '../../../common/services/editorWorker.js';
import { ITextModelService } from '../../../common/services/resolverService.js';
import { FormattingEdit } from './formattingEdit.js';
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
import { ExtensionIdentifierSet } from '../../../../platform/extensions/common/extensions.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { ILanguageFeaturesService } from '../../../common/services/languageFeatures.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { AudioCue, IAudioCueService } from '../../../../platform/audioCues/browser/audioCueService.js';
export function getRealAndSyntheticDocumentFormattersOrdered(documentFormattingEditProvider, documentRangeFormattingEditProvider, model) {
const result = [];
const seen = new ExtensionIdentifierSet();
// (1) add all document formatter
const docFormatter = documentFormattingEditProvider.ordered(model);
for (const formatter of docFormatter) {
result.push(formatter);
if (formatter.extensionId) {
seen.add(formatter.extensionId);
}
}
// (2) add all range formatter as document formatter (unless the same extension already did that)
const rangeFormatter = documentRangeFormattingEditProvider.ordered(model);
for (const formatter of rangeFormatter) {
if (formatter.extensionId) {
if (seen.has(formatter.extensionId)) {
continue;
}
seen.add(formatter.extensionId);
}
result.push({
displayName: formatter.displayName,
extensionId: formatter.extensionId,
provideDocumentFormattingEdits(model, options, token) {
return formatter.provideDocumentRangeFormattingEdits(model, model.getFullModelRange(), options, token);
}
});
}
return result;
}
export class FormattingConflicts {
static setFormatterSelector(selector) {
const remove = FormattingConflicts._selectors.unshift(selector);
return { dispose: remove };
}
static async select(formatter, document, mode, kind) {
if (formatter.length === 0) {
return undefined;
}
const selector = Iterable.first(FormattingConflicts._selectors);
if (selector) {
return await selector(formatter, document, mode, kind);
}
return undefined;
}
}
FormattingConflicts._selectors = new LinkedList();
export async function formatDocumentRangesWithSelectedProvider(accessor, editorOrModel, rangeOrRanges, mode, progress, token, userGesture) {
const instaService = accessor.get(IInstantiationService);
const { documentRangeFormattingEditProvider: documentRangeFormattingEditProviderRegistry } = accessor.get(ILanguageFeaturesService);
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
const provider = documentRangeFormattingEditProviderRegistry.ordered(model);
const selected = await FormattingConflicts.select(provider, model, mode, 2 /* FormattingKind.Selection */);
if (selected) {
progress.report(selected);
await instaService.invokeFunction(formatDocumentRangesWithProvider, selected, editorOrModel, rangeOrRanges, token, userGesture);
}
}
export async function formatDocumentRangesWithProvider(accessor, provider, editorOrModel, rangeOrRanges, token, userGesture) {
var _a, _b;
const workerService = accessor.get(IEditorWorkerService);
const logService = accessor.get(ILogService);
const audioCueService = accessor.get(IAudioCueService);
let model;
let cts;
if (isCodeEditor(editorOrModel)) {
model = editorOrModel.getModel();
cts = new EditorStateCancellationTokenSource(editorOrModel, 1 /* CodeEditorStateFlag.Value */ | 4 /* CodeEditorStateFlag.Position */, undefined, token);
}
else {
model = editorOrModel;
cts = new TextModelCancellationTokenSource(editorOrModel, token);
}
// make sure that ranges don't overlap nor touch each other
const ranges = [];
let len = 0;
for (const range of asArray(rangeOrRanges).sort(Range.compareRangesUsingStarts)) {
if (len > 0 && Range.areIntersectingOrTouching(ranges[len - 1], range)) {
ranges[len - 1] = Range.fromPositions(ranges[len - 1].getStartPosition(), range.getEndPosition());
}
else {
len = ranges.push(range);
}
}
const computeEdits = async (range) => {
var _a, _b;
logService.trace(`[format][provideDocumentRangeFormattingEdits] (request)`, (_a = provider.extensionId) === null || _a === void 0 ? void 0 : _a.value, range);
const result = (await provider.provideDocumentRangeFormattingEdits(model, range, model.getFormattingOptions(), cts.token)) || [];
logService.trace(`[format][provideDocumentRangeFormattingEdits] (response)`, (_b = provider.extensionId) === null || _b === void 0 ? void 0 : _b.value, result);
return result;
};
const hasIntersectingEdit = (a, b) => {
if (!a.length || !b.length) {
return false;
}
// quick exit if the list of ranges are completely unrelated [O(n)]
const mergedA = a.reduce((acc, val) => { return Range.plusRange(acc, val.range); }, a[0].range);
if (!b.some(x => { return Range.intersectRanges(mergedA, x.range); })) {
return false;
}
// fallback to a complete check [O(n^2)]
for (const edit of a) {
for (const otherEdit of b) {
if (Range.intersectRanges(edit.range, otherEdit.range)) {
return true;
}
}
}
return false;
};
const allEdits = [];
const rawEditsList = [];
try {
if (typeof provider.provideDocumentRangesFormattingEdits === 'function') {
logService.trace(`[format][provideDocumentRangeFormattingEdits] (request)`, (_a = provider.extensionId) === null || _a === void 0 ? void 0 : _a.value, ranges);
const result = (await provider.provideDocumentRangesFormattingEdits(model, ranges, model.getFormattingOptions(), cts.token)) || [];
logService.trace(`[format][provideDocumentRangeFormattingEdits] (response)`, (_b = provider.extensionId) === null || _b === void 0 ? void 0 : _b.value, result);
rawEditsList.push(result);
}
else {
for (const range of ranges) {
if (cts.token.isCancellationRequested) {
return true;
}
rawEditsList.push(await computeEdits(range));
}
for (let i = 0; i < ranges.length; ++i) {
for (let j = i + 1; j < ranges.length; ++j) {
if (cts.token.isCancellationRequested) {
return true;
}
if (hasIntersectingEdit(rawEditsList[i], rawEditsList[j])) {
// Merge ranges i and j into a single range, recompute the associated edits
const mergedRange = Range.plusRange(ranges[i], ranges[j]);
const edits = await computeEdits(mergedRange);
ranges.splice(j, 1);
ranges.splice(i, 1);
ranges.push(mergedRange);
rawEditsList.splice(j, 1);
rawEditsList.splice(i, 1);
rawEditsList.push(edits);
// Restart scanning
i = 0;
j = 0;
}
}
}
}
for (const rawEdits of rawEditsList) {
if (cts.token.isCancellationRequested) {
return true;
}
const minimalEdits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
if (minimalEdits) {
allEdits.push(...minimalEdits);
}
}
}
finally {
cts.dispose();
}
if (allEdits.length === 0) {
return false;
}
if (isCodeEditor(editorOrModel)) {
// use editor to apply edits
FormattingEdit.execute(editorOrModel, allEdits, true);
editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), 1 /* ScrollType.Immediate */);
}
else {
// use model to apply edits
const [{ range }] = allEdits;
const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
model.pushEditOperations([initialSelection], allEdits.map(edit => {
return {
text: edit.text,
range: Range.lift(edit.range),
forceMoveMarkers: true
};
}), undoEdits => {
for (const { range } of undoEdits) {
if (Range.areIntersectingOrTouching(range, initialSelection)) {
return [new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)];
}
}
return null;
});
}
audioCueService.playAudioCue(AudioCue.format, { userGesture });
return true;
}
export async function formatDocumentWithSelectedProvider(accessor, editorOrModel, mode, progress, token, userGesture) {
const instaService = accessor.get(IInstantiationService);
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
const model = isCodeEditor(editorOrModel) ? editorOrModel.getModel() : editorOrModel;
const provider = getRealAndSyntheticDocumentFormattersOrdered(languageFeaturesService.documentFormattingEditProvider, languageFeaturesService.documentRangeFormattingEditProvider, model);
const selected = await FormattingConflicts.select(provider, model, mode, 1 /* FormattingKind.File */);
if (selected) {
progress.report(selected);
await instaService.invokeFunction(formatDocumentWithProvider, selected, editorOrModel, mode, token, userGesture);
}
}
export async function formatDocumentWithProvider(accessor, provider, editorOrModel, mode, token, userGesture) {
const workerService = accessor.get(IEditorWorkerService);
const audioCueService = accessor.get(IAudioCueService);
let model;
let cts;
if (isCodeEditor(editorOrModel)) {
model = editorOrModel.getModel();
cts = new EditorStateCancellationTokenSource(editorOrModel, 1 /* CodeEditorStateFlag.Value */ | 4 /* CodeEditorStateFlag.Position */, undefined, token);
}
else {
model = editorOrModel;
cts = new TextModelCancellationTokenSource(editorOrModel, token);
}
let edits;
try {
const rawEdits = await provider.provideDocumentFormattingEdits(model, model.getFormattingOptions(), cts.token);
edits = await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
if (cts.token.isCancellationRequested) {
return true;
}
}
finally {
cts.dispose();
}
if (!edits || edits.length === 0) {
return false;
}
if (isCodeEditor(editorOrModel)) {
// use editor to apply edits
FormattingEdit.execute(editorOrModel, edits, mode !== 2 /* FormattingMode.Silent */);
if (mode !== 2 /* FormattingMode.Silent */) {
editorOrModel.revealPositionInCenterIfOutsideViewport(editorOrModel.getPosition(), 1 /* ScrollType.Immediate */);
}
}
else {
// use model to apply edits
const [{ range }] = edits;
const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);
model.pushEditOperations([initialSelection], edits.map(edit => {
return {
text: edit.text,
range: Range.lift(edit.range),
forceMoveMarkers: true
};
}), undoEdits => {
for (const { range } of undoEdits) {
if (Range.areIntersectingOrTouching(range, initialSelection)) {
return [new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)];
}
}
return null;
});
}
audioCueService.playAudioCue(AudioCue.format, { userGesture });
return true;
}
export async function getDocumentRangeFormattingEditsUntilResult(workerService, languageFeaturesService, model, range, options, token) {
const providers = languageFeaturesService.documentRangeFormattingEditProvider.ordered(model);
for (const provider of providers) {
const rawEdits = await Promise.resolve(provider.provideDocumentRangeFormattingEdits(model, range, options, token)).catch(onUnexpectedExternalError);
if (isNonEmptyArray(rawEdits)) {
return await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
}
}
return undefined;
}
export async function getDocumentFormattingEditsUntilResult(workerService, languageFeaturesService, model, options, token) {
const providers = getRealAndSyntheticDocumentFormattersOrdered(languageFeaturesService.documentFormattingEditProvider, languageFeaturesService.documentRangeFormattingEditProvider, model);
for (const provider of providers) {
const rawEdits = await Promise.resolve(provider.provideDocumentFormattingEdits(model, options, token)).catch(onUnexpectedExternalError);
if (isNonEmptyArray(rawEdits)) {
return await workerService.computeMoreMinimalEdits(model.uri, rawEdits);
}
}
return undefined;
}
export function getOnTypeFormattingEdits(workerService, languageFeaturesService, model, position, ch, options, token) {
const providers = languageFeaturesService.onTypeFormattingEditProvider.ordered(model);
if (providers.length === 0) {
return Promise.resolve(undefined);
}
if (providers[0].autoFormatTriggerCharacters.indexOf(ch) < 0) {
return Promise.resolve(undefined);
}
return Promise.resolve(providers[0].provideOnTypeFormattingEdits(model, position, ch, options, token)).catch(onUnexpectedExternalError).then(edits => {
return workerService.computeMoreMinimalEdits(model.uri, edits);
});
}
CommandsRegistry.registerCommand('_executeFormatRangeProvider', async function (accessor, ...args) {
const [resource, range, options] = args;
assertType(URI.isUri(resource));
assertType(Range.isIRange(range));
const resolverService = accessor.get(ITextModelService);
const workerService = accessor.get(IEditorWorkerService);
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
const reference = await resolverService.createModelReference(resource);
try {
return getDocumentRangeFormattingEditsUntilResult(workerService, languageFeaturesService, reference.object.textEditorModel, Range.lift(range), options, CancellationToken.None);
}
finally {
reference.dispose();
}
});
CommandsRegistry.registerCommand('_executeFormatDocumentProvider', async function (accessor, ...args) {
const [resource, options] = args;
assertType(URI.isUri(resource));
const resolverService = accessor.get(ITextModelService);
const workerService = accessor.get(IEditorWorkerService);
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
const reference = await resolverService.createModelReference(resource);
try {
return getDocumentFormattingEditsUntilResult(workerService, languageFeaturesService, reference.object.textEditorModel, options, CancellationToken.None);
}
finally {
reference.dispose();
}
});
CommandsRegistry.registerCommand('_executeFormatOnTypeProvider', async function (accessor, ...args) {
const [resource, position, ch, options] = args;
assertType(URI.isUri(resource));
assertType(Position.isIPosition(position));
assertType(typeof ch === 'string');
const resolverService = accessor.get(ITextModelService);
const workerService = accessor.get(IEditorWorkerService);
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
const reference = await resolverService.createModelReference(resource);
try {
return getOnTypeFormattingEdits(workerService, languageFeaturesService, reference.object.textEditorModel, Position.lift(position), ch, options, CancellationToken.None);
}
finally {
reference.dispose();
}
});