UNPKG

alm

Version:

The best IDE for TypeScript

310 lines (268 loc) 11.9 kB
/** * 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 }); } }); }); }