Files
vscode/src/vs/workbench/api/node/mainThreadEditors.ts
2016-05-22 14:24:18 +02:00

662 lines
20 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import EditorCommon = require('vs/editor/common/editorCommon');
import Event, {Emitter} from 'vs/base/common/event';
import {IEditor} from 'vs/platform/editor/common/editor';
import {ICodeEditorService} from 'vs/editor/common/services/codeEditorService';
import {IModelService} from 'vs/editor/common/services/modelService';
import {IDisposable, dispose} from 'vs/base/common/lifecycle';
import {RunOnceScheduler} from 'vs/base/common/async';
import {Range} from 'vs/editor/common/core/range';
import {Selection} from 'vs/editor/common/core/selection';
import {EndOfLine} from 'vs/workbench/api/node/extHostTypes';
export interface ITextEditorConfigurationUpdate {
tabSize?: number | string;
insertSpaces?: boolean | string;
cursorStyle?: EditorCommon.TextEditorCursorStyle;
}
export interface IResolvedTextEditorConfiguration {
tabSize: number;
insertSpaces: boolean;
cursorStyle: EditorCommon.TextEditorCursorStyle;
}
function configurationsEqual(a:IResolvedTextEditorConfiguration, b:IResolvedTextEditorConfiguration) {
if (a && !b || !a && b) {
return false;
}
if (!a && !b) {
return true;
}
return (
a.tabSize === b.tabSize
&& a.insertSpaces === b.insertSpaces
);
}
export interface IFocusTracker {
onGainedFocus(): void;
onLostFocus(): void;
}
export enum TextEditorRevealType {
Default,
InCenter,
InCenterIfOutsideViewport
}
/**
* Text Editor that is permanently bound to the same model.
* It can be bound or not to a CodeEditor.
*/
export class MainThreadTextEditor {
private _id: string;
private _model: EditorCommon.IModel;
private _modelService: IModelService;
private _modelListeners: IDisposable[];
private _codeEditor: EditorCommon.ICommonCodeEditor;
private _focusTracker: IFocusTracker;
private _codeEditorListeners: IDisposable[];
private _lastSelection: Selection[];
private _configuration: IResolvedTextEditorConfiguration;
private _onSelectionChanged: Emitter<Selection[]>;
private _onConfigurationChanged: Emitter<IResolvedTextEditorConfiguration>;
constructor(
id: string,
model:EditorCommon.IModel,
codeEditor:EditorCommon.ICommonCodeEditor,
focusTracker:IFocusTracker,
modelService: IModelService
) {
this._id = id;
this._model = model;
this._codeEditor = null;
this._focusTracker = focusTracker;
this._modelService = modelService;
this._codeEditorListeners = [];
this._onSelectionChanged = new Emitter<Selection[]>();
this._onConfigurationChanged = new Emitter<IResolvedTextEditorConfiguration>();
this._lastSelection = [ new Selection(1,1,1,1) ];
this._setConfiguration(this._readConfiguration(this._model, this._codeEditor));
this._modelListeners = [];
this._modelListeners.push(this._model.addListener2(EditorCommon.EventType.ModelOptionsChanged, (e) => {
this._setConfiguration(this._readConfiguration(this._model, this._codeEditor));
}));
this.setCodeEditor(codeEditor);
}
public dispose(): void {
this._model = null;
this._modelListeners = dispose(this._modelListeners);
this._codeEditor = null;
this._codeEditorListeners = dispose(this._codeEditorListeners);
}
public getId(): string {
return this._id;
}
public getModel(): EditorCommon.IModel {
return this._model;
}
public hasCodeEditor(codeEditor:EditorCommon.ICommonCodeEditor): boolean {
return (this._codeEditor === codeEditor);
}
public setCodeEditor(codeEditor:EditorCommon.ICommonCodeEditor): void {
if (this.hasCodeEditor(codeEditor)) {
// Nothing to do...
return;
}
this._codeEditorListeners = dispose(this._codeEditorListeners);
this._codeEditor = codeEditor;
if (this._codeEditor) {
// Catch early the case that this code editor gets a different model set and disassociate from this model
this._codeEditorListeners.push(this._codeEditor.onDidModelChange(() => {
this.setCodeEditor(null);
}));
let forwardSelection = () => {
this._lastSelection = this._codeEditor.getSelections();
this._onSelectionChanged.fire(this._lastSelection);
};
this._codeEditorListeners.push(this._codeEditor.onDidCursorSelectionChange(forwardSelection));
if (!Selection.selectionsArrEqual(this._lastSelection, this._codeEditor.getSelections())) {
forwardSelection();
}
this._codeEditorListeners.push(this._codeEditor.onDidEditorFocus(() => {
this._focusTracker.onGainedFocus();
}));
this._codeEditorListeners.push(this._codeEditor.onDidEditorBlur(() => {
this._focusTracker.onLostFocus();
}));
this._codeEditorListeners.push(this._codeEditor.onDidConfigurationChange(() => {
this._setConfiguration(this._readConfiguration(this._model, this._codeEditor));
}));
this._setConfiguration(this._readConfiguration(this._model, this._codeEditor));
}
}
public isVisible(): boolean {
return !!this._codeEditor;
}
public get onSelectionChanged(): Event<Selection[]> {
return this._onSelectionChanged.event;
}
public get onConfigurationChanged(): Event<IResolvedTextEditorConfiguration> {
return this._onConfigurationChanged.event;
}
public getSelections(): Selection[] {
if (this._codeEditor) {
return this._codeEditor.getSelections();
}
return this._lastSelection;
}
public setSelections(selections:EditorCommon.ISelection[]): void {
if (this._codeEditor) {
this._codeEditor.setSelections(selections);
return;
}
this._lastSelection = selections.map(Selection.liftSelection);
console.warn('setSelections on invisble editor');
}
public getConfiguration(): IResolvedTextEditorConfiguration {
return this._configuration;
}
private _setIndentConfiguration(newConfiguration:ITextEditorConfigurationUpdate): void {
if (newConfiguration.tabSize === 'auto' || newConfiguration.insertSpaces === 'auto') {
// one of the options was set to 'auto' => detect indentation
let creationOpts = this._modelService.getCreationOptions();
let insertSpaces = creationOpts.insertSpaces;
let tabSize = creationOpts.tabSize;
if (newConfiguration.insertSpaces !== 'auto') {
if (typeof newConfiguration.insertSpaces !== 'undefined') {
insertSpaces = (newConfiguration.insertSpaces === 'false' ? false : Boolean(newConfiguration.insertSpaces));
}
}
if (newConfiguration.tabSize !== 'auto') {
if (typeof newConfiguration.tabSize !== 'undefined') {
let parsedTabSize = parseInt(<string>newConfiguration.tabSize, 10);
if (!isNaN(parsedTabSize)) {
tabSize = parsedTabSize;
}
}
}
this._model.detectIndentation(insertSpaces, tabSize);
return;
}
let newOpts: EditorCommon.ITextModelUpdateOptions = {};
if (typeof newConfiguration.insertSpaces !== 'undefined') {
newOpts.insertSpaces = (newConfiguration.insertSpaces === 'false' ? false : Boolean(newConfiguration.insertSpaces));
}
if (typeof newConfiguration.tabSize !== 'undefined') {
let parsedTabSize = parseInt(<string>newConfiguration.tabSize, 10);
if (!isNaN(parsedTabSize)) {
newOpts.tabSize = parsedTabSize;
}
}
this._model.updateOptions(newOpts);
}
public setConfiguration(newConfiguration:ITextEditorConfigurationUpdate): void {
this._setIndentConfiguration(newConfiguration);
if (newConfiguration.cursorStyle) {
let newCursorStyle = EditorCommon.cursorStyleToString(newConfiguration.cursorStyle);
if (!this._codeEditor) {
console.warn('setConfiguration on invisible editor');
return;
}
this._codeEditor.updateOptions({
cursorStyle: newCursorStyle
});
}
}
public setDecorations(key: string, ranges:EditorCommon.IRangeWithMessage[]): void {
if (!this._codeEditor) {
console.warn('setDecorations on invisible editor');
return;
}
this._codeEditor.setDecorations(key, ranges);
}
public revealRange(range:EditorCommon.IRange, revealType:TextEditorRevealType): void {
if (!this._codeEditor) {
console.warn('revealRange on invisible editor');
return;
}
if (revealType === TextEditorRevealType.Default) {
this._codeEditor.revealRange(range);
} else if (revealType === TextEditorRevealType.InCenter) {
this._codeEditor.revealRangeInCenter(range);
} else if (revealType === TextEditorRevealType.InCenterIfOutsideViewport) {
this._codeEditor.revealRangeInCenterIfOutsideViewport(range);
} else {
console.warn('Unknown revealType');
}
}
private _readConfiguration(model:EditorCommon.IModel, codeEditor:EditorCommon.ICommonCodeEditor): IResolvedTextEditorConfiguration {
if (model.isDisposed()) {
// shutdown time
return this._configuration;
}
let cursorStyle = this._configuration ? this._configuration.cursorStyle : EditorCommon.TextEditorCursorStyle.Line;
if (codeEditor) {
let codeEditorOpts = codeEditor.getConfiguration();
cursorStyle = codeEditorOpts.viewInfo.cursorStyle;
}
let indent = model.getOptions();
return {
insertSpaces: indent.insertSpaces,
tabSize: indent.tabSize,
cursorStyle: cursorStyle
};
}
private _setConfiguration(newConfiguration:IResolvedTextEditorConfiguration): void {
if (configurationsEqual(this._configuration, newConfiguration)) {
return;
}
this._configuration = newConfiguration;
this._onConfigurationChanged.fire(this._configuration);
}
public isFocused(): boolean {
if (this._codeEditor) {
return this._codeEditor.isFocused();
}
return false;
}
public matches(editor: IEditor): boolean {
if (!editor) {
return false;
}
return editor.getControl() === this._codeEditor;
}
public applyEdits(versionIdCheck:number, edits:EditorCommon.ISingleEditOperation[], setEndOfLine:EndOfLine): boolean {
if (this._model.getVersionId() !== versionIdCheck) {
console.warn('Model has changed in the meantime!');
// throw new Error('Model has changed in the meantime!');
// model changed in the meantime
return false;
}
if (this._codeEditor) {
if (setEndOfLine === EndOfLine.CRLF) {
this._model.setEOL(EditorCommon.EndOfLineSequence.CRLF);
} else if (setEndOfLine === EndOfLine.LF) {
this._model.setEOL(EditorCommon.EndOfLineSequence.LF);
}
let transformedEdits = edits.map((edit): EditorCommon.IIdentifiedSingleEditOperation => {
return {
identifier: null,
range: Range.lift(edit.range),
text: edit.text,
forceMoveMarkers: edit.forceMoveMarkers
};
});
return this._codeEditor.executeEdits('MainThreadTextEditor', transformedEdits) || true;
}
console.warn('applyEdits on invisible editor');
return false;
}
}
/**
* Keeps track of what goes on in the main thread and maps models => text editors
*/
export class MainThreadEditorsTracker {
private static _LAST_TEXT_EDITOR_ID = 0;
private _nextId(): string {
return String(++MainThreadEditorsTracker._LAST_TEXT_EDITOR_ID);
}
private _toDispose: IDisposable[];
private _codeEditorService: ICodeEditorService;
private _modelService: IModelService;
private _updateMapping: RunOnceScheduler;
private _editorModelChangeListeners: {[editorId:string]:IDisposable;};
private _model2TextEditors: {
[modelUri:string]: MainThreadTextEditor[];
};
private _focusedTextEditorId: string;
private _visibleTextEditorIds: string[];
private _onTextEditorAdd: Emitter<MainThreadTextEditor>;
private _onTextEditorRemove: Emitter<MainThreadTextEditor>;
private _onDidChangeFocusedTextEditor: Emitter<string>;
private _onDidUpdateTextEditors: Emitter<void>;
private _focusTracker: IFocusTracker;
constructor(
editorService:ICodeEditorService,
modelService:IModelService
) {
this._codeEditorService = editorService;
this._modelService = modelService;
this._toDispose = [];
this._focusedTextEditorId = null;
this._visibleTextEditorIds = [];
this._editorModelChangeListeners = Object.create(null);
this._model2TextEditors = Object.create(null);
this._onTextEditorAdd = new Emitter<MainThreadTextEditor>();
this._onTextEditorRemove = new Emitter<MainThreadTextEditor>();
this._onDidUpdateTextEditors = new Emitter<void>();
this._onDidChangeFocusedTextEditor = new Emitter<string>();
this._focusTracker = {
onGainedFocus: () => this._updateFocusedTextEditor(),
onLostFocus: () => this._updateFocusedTextEditor()
};
this._modelService.onModelAdded(this._onModelAdded, this, this._toDispose);
this._modelService.onModelRemoved(this._onModelRemoved, this, this._toDispose);
this._codeEditorService.onCodeEditorAdd(this._onCodeEditorAdd, this, this._toDispose);
this._codeEditorService.onCodeEditorRemove(this._onCodeEditorRemove, this, this._toDispose);
this._updateMapping = new RunOnceScheduler(() => this._doUpdateMapping(), 0);
this._toDispose.push(this._updateMapping);
}
public dispose(): void {
this._toDispose = dispose(this._toDispose);
}
private _onModelAdded(model: EditorCommon.IModel): void {
this._updateMapping.schedule();
}
private _onModelRemoved(model: EditorCommon.IModel): void {
this._updateMapping.schedule();
}
private _onCodeEditorAdd(codeEditor: EditorCommon.ICommonCodeEditor): void {
this._editorModelChangeListeners[codeEditor.getId()] = codeEditor.onDidModelChange(_ => this._updateMapping.schedule());
this._updateMapping.schedule();
}
private _onCodeEditorRemove(codeEditor: EditorCommon.ICommonCodeEditor): void {
this._editorModelChangeListeners[codeEditor.getId()].dispose();
delete this._editorModelChangeListeners[codeEditor.getId()];
this._updateMapping.schedule();
}
private _doUpdateMapping(): void {
let allModels = this._modelService.getModels();
// Same filter as in extHostDocuments
allModels = allModels.filter((model) => !model.isTooLargeForHavingARichMode());
let allModelsMap: { [modelUri:string]: EditorCommon.IModel; } = Object.create(null);
allModels.forEach((model) => {
allModelsMap[model.uri.toString()] = model;
});
// Remove text editors for models that no longer exist
Object.keys(this._model2TextEditors).forEach((modelUri) => {
if (allModelsMap[modelUri]) {
// model still exists, will be updated below
return;
}
let textEditorsToRemove = this._model2TextEditors[modelUri];
delete this._model2TextEditors[modelUri];
for (let i = 0; i < textEditorsToRemove.length; i++) {
this._onTextEditorRemove.fire(textEditorsToRemove[i]);
textEditorsToRemove[i].dispose();
}
});
// Handle all visible models
let visibleModels = this._getVisibleModels();
Object.keys(visibleModels).forEach((modelUri) => {
let model = visibleModels[modelUri].model;
let codeEditors = visibleModels[modelUri].codeEditors;
if (!this._model2TextEditors[modelUri]) {
this._model2TextEditors[modelUri] = [];
}
let existingTextEditors = this._model2TextEditors[modelUri];
// Remove text editors if more exist
while (existingTextEditors.length > codeEditors.length) {
let removedTextEditor = existingTextEditors.pop();
this._onTextEditorRemove.fire(removedTextEditor);
removedTextEditor.dispose();
}
// Adjust remaining text editors
for (let i = 0; i < existingTextEditors.length; i++) {
existingTextEditors[i].setCodeEditor(codeEditors[i]);
}
// Create new editors as needed
for (let i = existingTextEditors.length; i < codeEditors.length; i++) {
let newTextEditor = new MainThreadTextEditor(this._nextId(), model, codeEditors[i], this._focusTracker, this._modelService);
existingTextEditors.push(newTextEditor);
this._onTextEditorAdd.fire(newTextEditor);
}
});
// Handle all not visible models
allModels.forEach((model) => {
let modelUri = model.uri.toString();
if (visibleModels[modelUri]) {
// model is visible, already handled above
return;
}
if (!this._model2TextEditors[modelUri]) {
this._model2TextEditors[modelUri] = [];
}
let existingTextEditors = this._model2TextEditors[modelUri];
// Remove extra text editors
while (existingTextEditors.length > 1) {
let removedTextEditor = existingTextEditors.pop();
this._onTextEditorRemove.fire(removedTextEditor);
removedTextEditor.dispose();
}
// Create new editor if needed or adjust it
if (existingTextEditors.length === 0) {
let newTextEditor = new MainThreadTextEditor(this._nextId(), model, null, this._focusTracker, this._modelService);
existingTextEditors.push(newTextEditor);
this._onTextEditorAdd.fire(newTextEditor);
} else {
existingTextEditors[0].setCodeEditor(null);
}
});
this._printState();
this._visibleTextEditorIds = this._findVisibleTextEditorIds();
this._updateFocusedTextEditor();
// this is a sync event
this._onDidUpdateTextEditors.fire(undefined);
}
private _updateFocusedTextEditor(): void {
this._setFocusedTextEditorId(this._findFocusedTextEditorId());
}
private _findFocusedTextEditorId(): string {
let modelUris = Object.keys(this._model2TextEditors);
for (let i = 0, len = modelUris.length; i < len; i++) {
let editors = this._model2TextEditors[modelUris[i]];
for (let j = 0, lenJ = editors.length; j < lenJ; j++) {
if (editors[j].isFocused()) {
return editors[j].getId();
}
}
}
return null;
}
private _findVisibleTextEditorIds(): string[] {
let result = [];
let modelUris = Object.keys(this._model2TextEditors);
for (let i = 0, len = modelUris.length; i < len; i++) {
let editors = this._model2TextEditors[modelUris[i]];
for (let j = 0, lenJ = editors.length; j < lenJ; j++) {
if (editors[j].isVisible()) {
result.push(editors[j].getId());
}
}
}
result.sort();
return result;
}
private _setFocusedTextEditorId(focusedTextEditorId:string): void {
if (this._focusedTextEditorId === focusedTextEditorId) {
// no change
return;
}
this._focusedTextEditorId = focusedTextEditorId;
this._printState();
this._onDidChangeFocusedTextEditor.fire(this._focusedTextEditorId);
}
private _printState(): void {
// console.log('----------------------');
// Object.keys(this._model2TextEditors).forEach((modelUri) => {
// let editors = this._model2TextEditors[modelUri];
// console.log(editors.map((e) => {
// return e.getId() + " (" + (e.getId() === this._focusedTextEditorId ? 'FOCUSED, ': '') + modelUri + ")";
// }).join('\n'));
// });
}
private _getVisibleModels(): IVisibleModels {
let r: IVisibleModels = {};
let allCodeEditors = this._codeEditorService.listCodeEditors();
// Maintain a certain sorting such that the mapping doesn't change too much all the time
allCodeEditors.sort((a, b) => strcmp(a.getId(), b.getId()));
allCodeEditors.forEach((codeEditor) => {
let model = codeEditor.getModel();
if (!model || model.isTooLargeForHavingARichMode()) {
return;
}
let modelUri = model.uri.toString();
r[modelUri] = r[modelUri] || {
model: model,
codeEditors: []
};
r[modelUri].codeEditors.push(codeEditor);
});
return r;
}
public getFocusedTextEditorId(): string {
return this._focusedTextEditorId;
}
public getVisibleTextEditorIds(): string[] {
return this._visibleTextEditorIds;
}
public get onTextEditorAdd(): Event<MainThreadTextEditor> {
return this._onTextEditorAdd.event;
}
public get onTextEditorRemove(): Event<MainThreadTextEditor> {
return this._onTextEditorRemove.event;
}
public get onDidUpdateTextEditors(): Event<void> {
return this._onDidUpdateTextEditors.event;
}
public get onChangedFocusedTextEditor(): Event<string> {
return this._onDidChangeFocusedTextEditor.event;
}
public findTextEditorIdFor(codeEditor:EditorCommon.ICommonCodeEditor): string {
let modelUris = Object.keys(this._model2TextEditors);
for (let i = 0, len = modelUris.length; i < len; i++) {
let editors = this._model2TextEditors[modelUris[i]];
for (let j = 0, lenJ = editors.length; j < lenJ; j++) {
if (editors[j].hasCodeEditor(codeEditor)) {
return editors[j].getId();
}
}
}
return null;
}
public registerTextEditorDecorationType(key:string, options: EditorCommon.IDecorationRenderOptions): void {
this._codeEditorService.registerDecorationType(key, options);
}
public removeTextEditorDecorationType(key:string): void {
this._codeEditorService.removeDecorationType(key);
}
}
interface IVisibleModels {
[modelUri:string]: {
model: EditorCommon.IModel;
codeEditors: EditorCommon.ICommonCodeEditor[];
};
}
function strcmp(a:string, b:string): number {
if (a < b) {
return -1;
}
if (a > b) {
return 1;
}
return 0;
}