Path: blob/main/src/vs/editor/common/services/modelService.ts
5221 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter, Event } from '../../../base/common/event.js';6import { StringSHA1 } from '../../../base/common/hash.js';7import { Disposable, DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';8import { Schemas } from '../../../base/common/network.js';9import { equals } from '../../../base/common/objects.js';10import * as platform from '../../../base/common/platform.js';11import { URI } from '../../../base/common/uri.js';12import { IConfigurationChangeEvent, IConfigurationService } from '../../../platform/configuration/common/configuration.js';13import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';14import { IUndoRedoService, ResourceEditStackSnapshot } from '../../../platform/undoRedo/common/undoRedo.js';15import { clampedInt } from '../config/editorOptions.js';16import { EditOperation, ISingleEditOperation } from '../core/editOperation.js';17import { EDITOR_MODEL_DEFAULTS } from '../core/misc/textModelDefaults.js';18import { Range } from '../core/range.js';19import { ILanguageSelection } from '../languages/language.js';20import { PLAINTEXT_LANGUAGE_ID } from '../languages/modesRegistry.js';21import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from '../model.js';22import { isEditStackElement } from '../model/editStack.js';23import { TextModel, createTextBuffer } from '../model/textModel.js';24import { EditSources, TextModelEditSource } from '../textModelEditSource.js';25import { IModelLanguageChangedEvent } from '../textModelEvents.js';26import { IModelService } from './model.js';27import { ITextResourcePropertiesService } from './textResourceConfiguration.js';2829function MODEL_ID(resource: URI): string {30return resource.toString();31}3233class ModelData implements IDisposable {3435private readonly _modelEventListeners = new DisposableStore();3637constructor(38public readonly model: TextModel,39onWillDispose: (model: ITextModel) => void,40onDidChangeLanguage: (model: ITextModel, e: IModelLanguageChangedEvent) => void41) {42this.model = model;43this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model)));44this._modelEventListeners.add(model.onDidChangeLanguage((e) => onDidChangeLanguage(model, e)));45}4647public dispose(): void {48this._modelEventListeners.dispose();49}50}5152interface IRawEditorConfig {53tabSize?: unknown;54indentSize?: unknown;55insertSpaces?: unknown;56detectIndentation?: unknown;57trimAutoWhitespace?: unknown;58creationOptions?: unknown;59largeFileOptimizations?: unknown;60bracketPairColorization?: unknown;61}6263interface IRawConfig {64eol?: unknown;65editor?: IRawEditorConfig;66}6768const DEFAULT_EOL = (platform.isLinux || platform.isMacintosh) ? DefaultEndOfLine.LF : DefaultEndOfLine.CRLF;6970class DisposedModelInfo {71constructor(72public readonly uri: URI,73public readonly initialUndoRedoSnapshot: ResourceEditStackSnapshot | null,74public readonly time: number,75public readonly sharesUndoRedoStack: boolean,76public readonly heapSize: number,77public readonly sha1: string,78public readonly versionId: number,79public readonly alternativeVersionId: number,80) { }81}8283export class ModelService extends Disposable implements IModelService {8485public static MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK = 20 * 1024 * 1024;8687public _serviceBrand: undefined;8889private readonly _onModelAdded: Emitter<ITextModel> = this._register(new Emitter<ITextModel>());90public readonly onModelAdded: Event<ITextModel> = this._onModelAdded.event;9192private readonly _onModelRemoved: Emitter<ITextModel> = this._register(new Emitter<ITextModel>());93public readonly onModelRemoved: Event<ITextModel> = this._onModelRemoved.event;9495private readonly _onModelModeChanged = this._register(new Emitter<{ model: ITextModel; oldLanguageId: string }>());96public readonly onModelLanguageChanged = this._onModelModeChanged.event;9798private _modelCreationOptionsByLanguageAndResource: { [languageAndResource: string]: ITextModelCreationOptions };99100/**101* All the models known in the system.102*/103private readonly _models: { [modelId: string]: ModelData };104private readonly _disposedModels: Map<string, DisposedModelInfo>;105private _disposedModelsHeapSize: number;106107constructor(108@IConfigurationService private readonly _configurationService: IConfigurationService,109@ITextResourcePropertiesService private readonly _resourcePropertiesService: ITextResourcePropertiesService,110@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,111@IInstantiationService private readonly _instantiationService: IInstantiationService112) {113super();114this._modelCreationOptionsByLanguageAndResource = Object.create(null);115this._models = {};116this._disposedModels = new Map<string, DisposedModelInfo>();117this._disposedModelsHeapSize = 0;118119this._register(this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions(e)));120this._updateModelOptions(undefined);121}122123private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions {124let tabSize = EDITOR_MODEL_DEFAULTS.tabSize;125if (config.editor && typeof config.editor.tabSize !== 'undefined') {126tabSize = clampedInt(config.editor.tabSize, EDITOR_MODEL_DEFAULTS.tabSize, 1, 100);127}128129let indentSize: number | 'tabSize' = 'tabSize';130if (config.editor && typeof config.editor.indentSize !== 'undefined' && config.editor.indentSize !== 'tabSize') {131indentSize = clampedInt(config.editor.indentSize, 'tabSize', 1, 100);132}133134let insertSpaces = EDITOR_MODEL_DEFAULTS.insertSpaces;135if (config.editor && typeof config.editor.insertSpaces !== 'undefined') {136insertSpaces = (config.editor.insertSpaces === 'false' ? false : Boolean(config.editor.insertSpaces));137}138139let newDefaultEOL = DEFAULT_EOL;140const eol = config.eol;141if (eol === '\r\n') {142newDefaultEOL = DefaultEndOfLine.CRLF;143} else if (eol === '\n') {144newDefaultEOL = DefaultEndOfLine.LF;145}146147let trimAutoWhitespace = EDITOR_MODEL_DEFAULTS.trimAutoWhitespace;148if (config.editor && typeof config.editor.trimAutoWhitespace !== 'undefined') {149trimAutoWhitespace = (config.editor.trimAutoWhitespace === 'false' ? false : Boolean(config.editor.trimAutoWhitespace));150}151152let detectIndentation = EDITOR_MODEL_DEFAULTS.detectIndentation;153if (config.editor && typeof config.editor.detectIndentation !== 'undefined') {154detectIndentation = (config.editor.detectIndentation === 'false' ? false : Boolean(config.editor.detectIndentation));155}156157let largeFileOptimizations = EDITOR_MODEL_DEFAULTS.largeFileOptimizations;158if (config.editor && typeof config.editor.largeFileOptimizations !== 'undefined') {159largeFileOptimizations = (config.editor.largeFileOptimizations === 'false' ? false : Boolean(config.editor.largeFileOptimizations));160}161let bracketPairColorizationOptions = EDITOR_MODEL_DEFAULTS.bracketPairColorizationOptions;162if (config.editor?.bracketPairColorization && typeof config.editor.bracketPairColorization === 'object') {163const bpConfig = config.editor.bracketPairColorization as { enabled?: unknown; independentColorPoolPerBracketType?: unknown };164bracketPairColorizationOptions = {165enabled: !!bpConfig.enabled,166independentColorPoolPerBracketType: !!bpConfig.independentColorPoolPerBracketType167};168}169170return {171isForSimpleWidget: isForSimpleWidget,172tabSize: tabSize,173indentSize: indentSize,174insertSpaces: insertSpaces,175detectIndentation: detectIndentation,176defaultEOL: newDefaultEOL,177trimAutoWhitespace: trimAutoWhitespace,178largeFileOptimizations: largeFileOptimizations,179bracketPairColorizationOptions180};181}182183private _getEOL(resource: URI | undefined, language: string): string {184if (resource) {185return this._resourcePropertiesService.getEOL(resource, language);186}187const eol = this._configurationService.getValue('files.eol', { overrideIdentifier: language });188if (eol && typeof eol === 'string' && eol !== 'auto') {189return eol;190}191return platform.OS === platform.OperatingSystem.Linux || platform.OS === platform.OperatingSystem.Macintosh ? '\n' : '\r\n';192}193194private _shouldRestoreUndoStack(): boolean {195const result = this._configurationService.getValue('files.restoreUndoStack');196if (typeof result === 'boolean') {197return result;198}199return true;200}201202public getCreationOptions(languageIdOrSelection: string | ILanguageSelection, resource: URI | undefined, isForSimpleWidget: boolean): ITextModelCreationOptions {203const language = (typeof languageIdOrSelection === 'string' ? languageIdOrSelection : languageIdOrSelection.languageId);204let creationOptions = this._modelCreationOptionsByLanguageAndResource[language + resource];205if (!creationOptions) {206const editor = this._configurationService.getValue<IRawEditorConfig>('editor', { overrideIdentifier: language, resource });207const eol = this._getEOL(resource, language);208creationOptions = ModelService._readModelOptions({ editor, eol }, isForSimpleWidget);209this._modelCreationOptionsByLanguageAndResource[language + resource] = creationOptions;210}211return creationOptions;212}213214private _updateModelOptions(e: IConfigurationChangeEvent | undefined): void {215const oldOptionsByLanguageAndResource = this._modelCreationOptionsByLanguageAndResource;216this._modelCreationOptionsByLanguageAndResource = Object.create(null);217218// Update options on all models219const keys = Object.keys(this._models);220for (let i = 0, len = keys.length; i < len; i++) {221const modelId = keys[i];222const modelData = this._models[modelId];223const language = modelData.model.getLanguageId();224const uri = modelData.model.uri;225226if (e && !e.affectsConfiguration('editor', { overrideIdentifier: language, resource: uri }) && !e.affectsConfiguration('files.eol', { overrideIdentifier: language, resource: uri })) {227continue; // perf: skip if this model is not affected by configuration change228}229230const oldOptions = oldOptionsByLanguageAndResource[language + uri];231const newOptions = this.getCreationOptions(language, uri, modelData.model.isForSimpleWidget);232ModelService._setModelOptionsForModel(modelData.model, newOptions, oldOptions);233}234}235236private static _setModelOptionsForModel(model: ITextModel, newOptions: ITextModelCreationOptions, currentOptions: ITextModelCreationOptions): void {237if (currentOptions && currentOptions.defaultEOL !== newOptions.defaultEOL && model.getLineCount() === 1) {238model.setEOL(newOptions.defaultEOL === DefaultEndOfLine.LF ? EndOfLineSequence.LF : EndOfLineSequence.CRLF);239}240241if (currentOptions242&& (currentOptions.detectIndentation === newOptions.detectIndentation)243&& (currentOptions.insertSpaces === newOptions.insertSpaces)244&& (currentOptions.tabSize === newOptions.tabSize)245&& (currentOptions.indentSize === newOptions.indentSize)246&& (currentOptions.trimAutoWhitespace === newOptions.trimAutoWhitespace)247&& equals(currentOptions.bracketPairColorizationOptions, newOptions.bracketPairColorizationOptions)248) {249// Same indent opts, no need to touch the model250return;251}252253if (newOptions.detectIndentation) {254model.detectIndentation(newOptions.insertSpaces, newOptions.tabSize);255model.updateOptions({256trimAutoWhitespace: newOptions.trimAutoWhitespace,257bracketColorizationOptions: newOptions.bracketPairColorizationOptions258});259} else {260model.updateOptions({261insertSpaces: newOptions.insertSpaces,262tabSize: newOptions.tabSize,263indentSize: newOptions.indentSize,264trimAutoWhitespace: newOptions.trimAutoWhitespace,265bracketColorizationOptions: newOptions.bracketPairColorizationOptions266});267}268}269270// --- begin IModelService271272private _insertDisposedModel(disposedModelData: DisposedModelInfo): void {273this._disposedModels.set(MODEL_ID(disposedModelData.uri), disposedModelData);274this._disposedModelsHeapSize += disposedModelData.heapSize;275}276277private _removeDisposedModel(resource: URI): DisposedModelInfo | undefined {278const disposedModelData = this._disposedModels.get(MODEL_ID(resource));279if (disposedModelData) {280this._disposedModelsHeapSize -= disposedModelData.heapSize;281}282this._disposedModels.delete(MODEL_ID(resource));283return disposedModelData;284}285286private _ensureDisposedModelsHeapSize(maxModelsHeapSize: number): void {287if (this._disposedModelsHeapSize > maxModelsHeapSize) {288// we must remove some old undo stack elements to free up some memory289const disposedModels: DisposedModelInfo[] = [];290this._disposedModels.forEach(entry => {291if (!entry.sharesUndoRedoStack) {292disposedModels.push(entry);293}294});295disposedModels.sort((a, b) => a.time - b.time);296while (disposedModels.length > 0 && this._disposedModelsHeapSize > maxModelsHeapSize) {297const disposedModel = disposedModels.shift()!;298this._removeDisposedModel(disposedModel.uri);299if (disposedModel.initialUndoRedoSnapshot !== null) {300this._undoRedoService.restoreSnapshot(disposedModel.initialUndoRedoSnapshot);301}302}303}304}305306private _createModelData(value: string | ITextBufferFactory, languageIdOrSelection: string | ILanguageSelection, resource: URI | undefined, isForSimpleWidget: boolean): ModelData {307// create & save the model308const options = this.getCreationOptions(languageIdOrSelection, resource, isForSimpleWidget);309const model: TextModel = this._instantiationService.createInstance(TextModel,310value,311languageIdOrSelection,312options,313resource314);315if (resource && this._disposedModels.has(MODEL_ID(resource))) {316const disposedModelData = this._removeDisposedModel(resource)!;317const elements = this._undoRedoService.getElements(resource);318const sha1Computer = this._getSHA1Computer();319const sha1IsEqual = (320sha1Computer.canComputeSHA1(model)321? sha1Computer.computeSHA1(model) === disposedModelData.sha1322: false323);324if (sha1IsEqual || disposedModelData.sharesUndoRedoStack) {325for (const element of elements.past) {326if (isEditStackElement(element) && element.matchesResource(resource)) {327element.setModel(model);328}329}330for (const element of elements.future) {331if (isEditStackElement(element) && element.matchesResource(resource)) {332element.setModel(model);333}334}335this._undoRedoService.setElementsValidFlag(resource, true, (element) => (isEditStackElement(element) && element.matchesResource(resource)));336if (sha1IsEqual) {337model._overwriteVersionId(disposedModelData.versionId);338model._overwriteAlternativeVersionId(disposedModelData.alternativeVersionId);339model._overwriteInitialUndoRedoSnapshot(disposedModelData.initialUndoRedoSnapshot);340}341} else {342if (disposedModelData.initialUndoRedoSnapshot !== null) {343this._undoRedoService.restoreSnapshot(disposedModelData.initialUndoRedoSnapshot);344}345}346}347const modelId = MODEL_ID(model.uri);348349if (this._models[modelId]) {350// There already exists a model with this id => this is a programmer error351throw new Error('ModelService: Cannot add model because it already exists!');352}353354const modelData = new ModelData(355model,356(model) => this._onWillDispose(model),357(model, e) => this._onDidChangeLanguage(model, e)358);359this._models[modelId] = modelData;360361return modelData;362}363364public updateModel(model: ITextModel, value: string | ITextBufferFactory, reason: TextModelEditSource = EditSources.unknown({ name: 'updateModel' })): void {365const options = this.getCreationOptions(model.getLanguageId(), model.uri, model.isForSimpleWidget);366const { textBuffer, disposable } = createTextBuffer(value, options.defaultEOL);367368// Return early if the text is already set in that form369if (model.equalsTextBuffer(textBuffer)) {370disposable.dispose();371return;372}373374// Otherwise find a diff between the values and update model375model.pushStackElement();376model.pushEOL(textBuffer.getEOL() === '\r\n' ? EndOfLineSequence.CRLF : EndOfLineSequence.LF);377model.pushEditOperations(378[],379ModelService._computeEdits(model, textBuffer),380() => [],381undefined,382reason383);384model.pushStackElement();385disposable.dispose();386}387388private static _commonPrefix(a: ITextModel, aLen: number, aDelta: number, b: ITextBuffer, bLen: number, bDelta: number): number {389const maxResult = Math.min(aLen, bLen);390391let result = 0;392for (let i = 0; i < maxResult && a.getLineContent(aDelta + i) === b.getLineContent(bDelta + i); i++) {393result++;394}395return result;396}397398private static _commonSuffix(a: ITextModel, aLen: number, aDelta: number, b: ITextBuffer, bLen: number, bDelta: number): number {399const maxResult = Math.min(aLen, bLen);400401let result = 0;402for (let i = 0; i < maxResult && a.getLineContent(aDelta + aLen - i) === b.getLineContent(bDelta + bLen - i); i++) {403result++;404}405return result;406}407408/**409* Compute edits to bring `model` to the state of `textSource`.410*/411public static _computeEdits(model: ITextModel, textBuffer: ITextBuffer): ISingleEditOperation[] {412const modelLineCount = model.getLineCount();413const textBufferLineCount = textBuffer.getLineCount();414const commonPrefix = this._commonPrefix(model, modelLineCount, 1, textBuffer, textBufferLineCount, 1);415416if (modelLineCount === textBufferLineCount && commonPrefix === modelLineCount) {417// equality case418return [];419}420421const commonSuffix = this._commonSuffix(model, modelLineCount - commonPrefix, commonPrefix, textBuffer, textBufferLineCount - commonPrefix, commonPrefix);422423let oldRange: Range;424let newRange: Range;425if (commonSuffix > 0) {426oldRange = new Range(commonPrefix + 1, 1, modelLineCount - commonSuffix + 1, 1);427newRange = new Range(commonPrefix + 1, 1, textBufferLineCount - commonSuffix + 1, 1);428} else if (commonPrefix > 0) {429oldRange = new Range(commonPrefix, model.getLineMaxColumn(commonPrefix), modelLineCount, model.getLineMaxColumn(modelLineCount));430newRange = new Range(commonPrefix, 1 + textBuffer.getLineLength(commonPrefix), textBufferLineCount, 1 + textBuffer.getLineLength(textBufferLineCount));431} else {432oldRange = new Range(1, 1, modelLineCount, model.getLineMaxColumn(modelLineCount));433newRange = new Range(1, 1, textBufferLineCount, 1 + textBuffer.getLineLength(textBufferLineCount));434}435436return [EditOperation.replaceMove(oldRange, textBuffer.getValueInRange(newRange, EndOfLinePreference.TextDefined))];437}438439public createModel(value: string | ITextBufferFactory, languageSelection: ILanguageSelection | null, resource?: URI, isForSimpleWidget: boolean = false): ITextModel {440let modelData: ModelData;441442if (languageSelection) {443modelData = this._createModelData(value, languageSelection, resource, isForSimpleWidget);444} else {445modelData = this._createModelData(value, PLAINTEXT_LANGUAGE_ID, resource, isForSimpleWidget);446}447448this._onModelAdded.fire(modelData.model);449450return modelData.model;451}452453public destroyModel(resource: URI): void {454// We need to support that not all models get disposed through this service (i.e. model.dispose() should work!)455const modelData = this._models[MODEL_ID(resource)];456if (!modelData) {457return;458}459modelData.model.dispose();460}461462public getModels(): ITextModel[] {463const ret: ITextModel[] = [];464465const keys = Object.keys(this._models);466for (let i = 0, len = keys.length; i < len; i++) {467const modelId = keys[i];468ret.push(this._models[modelId].model);469}470471return ret;472}473474public getModel(resource: URI): ITextModel | null {475const modelId = MODEL_ID(resource);476const modelData = this._models[modelId];477if (!modelData) {478return null;479}480return modelData.model;481}482483// --- end IModelService484485protected _schemaShouldMaintainUndoRedoElements(resource: URI) {486return (487resource.scheme === Schemas.file488|| resource.scheme === Schemas.vscodeRemote489|| resource.scheme === Schemas.vscodeUserData490|| resource.scheme === Schemas.vscodeNotebookCell491|| resource.scheme === 'fake-fs' // for tests492);493}494495private _onWillDispose(model: ITextModel): void {496const modelId = MODEL_ID(model.uri);497const modelData = this._models[modelId];498499const sharesUndoRedoStack = (this._undoRedoService.getUriComparisonKey(model.uri) !== model.uri.toString());500let maintainUndoRedoStack = false;501let heapSize = 0;502if (sharesUndoRedoStack || (this._shouldRestoreUndoStack() && this._schemaShouldMaintainUndoRedoElements(model.uri))) {503const elements = this._undoRedoService.getElements(model.uri);504if (elements.past.length > 0 || elements.future.length > 0) {505for (const element of elements.past) {506if (isEditStackElement(element) && element.matchesResource(model.uri)) {507maintainUndoRedoStack = true;508heapSize += element.heapSize(model.uri);509element.setModel(model.uri); // remove reference from text buffer instance510}511}512for (const element of elements.future) {513if (isEditStackElement(element) && element.matchesResource(model.uri)) {514maintainUndoRedoStack = true;515heapSize += element.heapSize(model.uri);516element.setModel(model.uri); // remove reference from text buffer instance517}518}519}520}521522const maxMemory = ModelService.MAX_MEMORY_FOR_CLOSED_FILES_UNDO_STACK;523const sha1Computer = this._getSHA1Computer();524if (!maintainUndoRedoStack) {525if (!sharesUndoRedoStack) {526const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot();527if (initialUndoRedoSnapshot !== null) {528this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot);529}530}531} else if (!sharesUndoRedoStack && (heapSize > maxMemory || !sha1Computer.canComputeSHA1(model))) {532// the undo stack for this file would never fit in the configured memory or the file is very large, so don't bother with it.533const initialUndoRedoSnapshot = modelData.model.getInitialUndoRedoSnapshot();534if (initialUndoRedoSnapshot !== null) {535this._undoRedoService.restoreSnapshot(initialUndoRedoSnapshot);536}537} else {538this._ensureDisposedModelsHeapSize(maxMemory - heapSize);539// We only invalidate the elements, but they remain in the undo-redo service.540this._undoRedoService.setElementsValidFlag(model.uri, false, (element) => (isEditStackElement(element) && element.matchesResource(model.uri)));541this._insertDisposedModel(new DisposedModelInfo(model.uri, modelData.model.getInitialUndoRedoSnapshot(), Date.now(), sharesUndoRedoStack, heapSize, sha1Computer.computeSHA1(model), model.getVersionId(), model.getAlternativeVersionId()));542}543544delete this._models[modelId];545modelData.dispose();546547// clean up cache548delete this._modelCreationOptionsByLanguageAndResource[model.getLanguageId() + model.uri];549550this._onModelRemoved.fire(model);551}552553private _onDidChangeLanguage(model: ITextModel, e: IModelLanguageChangedEvent): void {554const oldLanguageId = e.oldLanguage;555const newLanguageId = model.getLanguageId();556const oldOptions = this.getCreationOptions(oldLanguageId, model.uri, model.isForSimpleWidget);557const newOptions = this.getCreationOptions(newLanguageId, model.uri, model.isForSimpleWidget);558ModelService._setModelOptionsForModel(model, newOptions, oldOptions);559this._onModelModeChanged.fire({ model, oldLanguageId: oldLanguageId });560}561562protected _getSHA1Computer(): ITextModelSHA1Computer {563return new DefaultModelSHA1Computer();564}565}566567export interface ITextModelSHA1Computer {568canComputeSHA1(model: ITextModel): boolean;569computeSHA1(model: ITextModel): string;570}571572export class DefaultModelSHA1Computer implements ITextModelSHA1Computer {573574public static MAX_MODEL_SIZE = 10 * 1024 * 1024; // takes 200ms to compute a sha1 on a 10MB model on a new machine575576canComputeSHA1(model: ITextModel): boolean {577return (model.getValueLength() <= DefaultModelSHA1Computer.MAX_MODEL_SIZE);578}579580computeSHA1(model: ITextModel): string {581// compute the sha1582const shaComputer = new StringSHA1();583const snapshot = model.createSnapshot();584let text: string | null;585while ((text = snapshot.read())) {586shaComputer.update(text);587}588return shaComputer.digest();589}590}591592593