alm
Version:
The best IDE for TypeScript
310 lines (268 loc) • 11.9 kB
text/typescript
/**
* We should have all the CM docs cached for consistent history and stuff
*/
import {TypedEvent} from "../../../common/events";
import {cast, server} from "../../../socket/socketClient";
import * as editBatcher from "./editBatcher";
import * as utils from "../../../common/utils";
import * as classifierCache from "./classifierCache";
import {RefactoringsByFilePath, Refactoring} from "../../../common/types";
import * as state from "../../state/state";
import {EditorOptions} from "../../../common/types";
import * as monacoUtils from "../../monaco/monacoUtils";
import * as events from "../../../common/events";
/**
* We extend the monaco editor model
*/
declare global {
module monaco {
module editor {
interface IReadOnlyModel {
/**
* keep `filePath`
* Beyond other things, it is critical to our tokenizer.
**/
filePath?: string;
/**
* add a list of editors
* - useful for restoring cursors if we edit the model
*/
_editors: monaco.editor.ICodeEditor[];
/**
* store the format code options so we can use them later
*/
_editorOptions: EditorOptions;
}
interface ICodeEditor {
}
}
}
}
/**
* Setup any additional languages
*/
import {setupMonacoTypecript} from "../../monaco/languages/typescript/monacoTypeScript";
setupMonacoTypecript();
import {setupMonacoJson} from "../../monaco/languages/json/monacoJson";
setupMonacoJson();
/**
* Ext lookup
*/
const extLookup: { [ext: string]: monaco.languages.ILanguageExtensionPoint } = {};
monaco.languages.getLanguages().forEach(function(language) {
// console.log(language); // DEBUG
language.extensions.forEach(ext => {
// ext is like `.foo`. We really want `foo`
ext = ext.substr(1);
extLookup[ext] = language;
})
});
/**
* Gets the mode for a give filePath (based on its ext)
*/
const getLanguage = (filePath: string): string => {
const mode = extLookup[utils.getExt(filePath)];
return mode ? mode.id : 'plaintext';
}
// to track the source of changes, local vs. network
const localSourceId: string = utils.createId();
export type GetLinkedDocResponse = {
doc: monaco.editor.IModel;
editorOptions: EditorOptions
}
export function getLinkedDoc(filePath: string,editor: monaco.editor.ICodeEditor): Promise<GetLinkedDocResponse> {
return getOrCreateDoc(filePath)
.then(({doc, isJsOrTsFile, editorOptions}) => {
// Everytime a typescript file is opened we ask for its output status (server pull)
// On editing this happens automatically (server push)
server.getJSOutputStatus({ filePath }).then(res => {
if (res.inActiveProject) {
state.fileOuputStatusUpdated(res.outputStatus);
}
});
/** Wire up the doc */
editor.setModel(doc);
/** Add to list of editors */
doc._editors.push(editor);
return {doc: doc, editorOptions: editorOptions};
});
}
export function removeLinkedDoc(filePath:string, editor: monaco.editor.ICodeEditor){
editor.getModel()._editors = editor.getModel()._editors.filter(e => e != editor);
// if this was the last editor using this model then we remove it from the cache as well
// otherwise we might get a file even if its deleted from the server
if (!editor.getModel()._editors.length){
docByFilePathPromised[filePath].then(x=>{
x.disposable.dispose();
delete docByFilePathPromised[filePath];
})
}
}
type DocPromiseResult = {
doc: monaco.editor.IModel,
isJsOrTsFile: boolean,
editorOptions: EditorOptions,
disposable: IDisposable,
}
let docByFilePathPromised: { [filePath: string]: Promise<DocPromiseResult> } = Object.create(null);
function getOrCreateDoc(filePath: string): Promise<DocPromiseResult> {
if (docByFilePathPromised[filePath]) {
return docByFilePathPromised[filePath];
}
else {
return docByFilePathPromised[filePath] = server.openFile({ filePath: filePath }).then((res) => {
const disposable = new events.CompositeDisposible();
let ext = utils.getExt(filePath);
let isJsOrTsFile = utils.isJsOrTs(filePath);
let language = getLanguage(filePath);
// console.log(res.editorOptions); // DEBUG
// console.log(mode,supportedModesMap[ext]); // Debug mode
// Add to classifier cache
if (isJsOrTsFile) {
classifierCache.addFile(filePath, res.contents);
disposable.add({ dispose: () => classifierCache.removeFile(filePath) });
}
// create the doc
const doc = monaco.editor.createModel(res.contents, language);
doc.setEOL(monaco.editor.EndOfLineSequence.LF); // The true eol is only with the file model at the backend. The frontend doesn't care 🌹
doc.filePath = filePath;
doc._editors = [];
doc._editorOptions = res.editorOptions;
/** Susbcribe to editor options changing */
disposable.add(cast.editorOptionsChanged.on((res) => {
if (res.filePath === filePath) {
doc._editorOptions = res.editorOptions;
}
}));
/**
* We ignore edit notifications from monaco if *we* did the edit e.g.
* - if the server sent the edit and we applied it.
*/
let countOfEditNotificationsToIgnore = 0;
/** This is used for monaco edit operation version counting purposes */
let editorOperationCounter = 0;
// setup to push doc changes to server
disposable.add(doc.onDidChangeContent(evt => {
// Keep the ouput status cache informed
state.ifJSStatusWasCurrentThenMoveToOutOfDate({inputFilePath: filePath});
// if this edit is happening
// because *we edited it due to a server request*
// we should exit
if (countOfEditNotificationsToIgnore) {
countOfEditNotificationsToIgnore--;
return;
}
let codeEdit: CodeEdit = {
from: { line: evt.range.startLineNumber - 1, ch: evt.range.startColumn - 1 },
to: { line: evt.range.endLineNumber - 1, ch: evt.range.endColumn - 1 },
newText: evt.text,
sourceId: localSourceId
};
// Send the edit
editBatcher.addToQueue(filePath, codeEdit);
// Keep the classifier in sync
if (isJsOrTsFile) { classifierCache.editFile(filePath, codeEdit) }
}));
// setup to get doc changes from server
disposable.add(cast.didEdits.on(res=> {
// console.log('got server edit', res.edit.sourceId,'our', sourceId)
let codeEdits = res.edits;
codeEdits.forEach(codeEdit => {
// Easy exit for local edits getting echoed back
if (res.filePath == filePath && codeEdit.sourceId !== localSourceId) {
// Keep the classifier in sync
if (isJsOrTsFile) { classifierCache.editFile(filePath, codeEdit); }
// make the edits
const editOperation: monaco.editor.IIdentifiedSingleEditOperation = {
identifier: {
major: editorOperationCounter++,
minor: 0
},
text: codeEdit.newText,
range: new monaco.Range(
codeEdit.from.line + 1,
codeEdit.from.ch + 1,
codeEdit.to.line + 1,
codeEdit.to.ch + 1
),
forceMoveMarkers: false,
isAutoWhitespaceEdit: false,
}
/** Mark as ignoring before applying the edit */
countOfEditNotificationsToIgnore++;
doc.pushEditOperations([], [editOperation], null);
}
});
}));
// setup loading saved files changing on disk
disposable.add(cast.savedFileChangedOnDisk.on((res) => {
if (res.filePath == filePath
&& doc.getValue() !== res.contents) {
// preserve cursor
doc._editors.forEach(e=>{
const cursors = e.getSelections();
setTimeout(()=>{
e.setSelections(cursors);
})
})
// Keep the classifier in sync
if (isJsOrTsFile) { classifierCache.setContents(filePath, res.contents); }
// Note that we use *mark as coming from server* so we don't go into doc.change handler later on :)
countOfEditNotificationsToIgnore++;
// NOTE: we don't do `setValue` as
// - that creates a new tokenizer (we would need to use `window.creatingModelFilePath`)
// - looses all undo history.
// Instead we *replace* all the text that is there 🌹
const totalDocRange = doc.getFullModelRange();
monacoUtils.replaceRange({
range: totalDocRange,
model: doc,
newText: res.contents
});
}
}));
// Finally return the doc
const result: DocPromiseResult = {
doc,
isJsOrTsFile,
editorOptions: res.editorOptions,
disposable: disposable,
};
return result;
});
}
}
/**
* Don't plan to export as giving others our true docs can have horrible consequences if they mess them up
*/
function getOrCreateDocs(filePaths: string[]): Promise<{ [filePath: string]: monaco.editor.IModel }> {
let promises = filePaths.map(fp => getOrCreateDoc(fp));
return Promise.all(promises).then(docs => {
let res: { [filePath: string]: monaco.editor.IModel } = {};
docs.forEach(({doc}) => res[doc.filePath] = doc);
return res;
});
}
export function applyRefactoringsToTsDocs(refactorings: RefactoringsByFilePath) {
let filePaths = Object.keys(refactorings);
getOrCreateDocs(filePaths).then(docsByFilePath => {
filePaths.forEach(fp=> {
let doc = docsByFilePath[fp];
let changes = refactorings[fp];
for (let change of changes) {
const from = classifierCache.getLineAndCharacterOfPosition(fp, change.span.start);
const to = classifierCache.getLineAndCharacterOfPosition(fp, change.span.start + change.span.length);
monacoUtils.replaceRange({
model: doc,
range: {
startLineNumber: from.line + 1,
startColumn: from.ch + 1,
endLineNumber: to.line + 1,
endColumn: to.ch + 1
},
newText: change.newText
});
}
});
});
}