// // Document.cs // // Author: // Lluis Sanchez Gual // // Copyright (C) 2005 Novell, Inc (http://www.novell.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the // "Software"), to deal in the Software without restriction, including // without limitation the rights to use, copy, modify, merge, publish, // distribute, sublicense, and/or sell copies of the Software, and to // permit persons to whom the Software is furnished to do so, subject to // the following conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // using System; using System.Collections; using System.Collections.Generic; using System.IO; using Gtk; using MonoDevelop.Core; using MonoDevelop.Core.Execution; using MonoDevelop.Components; using MonoDevelop.Projects; using MonoDevelop.Projects.Text; using MonoDevelop.Ide.Gui.Content; using MonoDevelop.Ide.Gui.Dialogs; using MonoDevelop.Ide.Tasks; using Mono.Addins; using MonoDevelop.Ide.Extensions; using System.Linq; using System.Threading; using MonoDevelop.Ide.TypeSystem; using ICSharpCode.NRefactory; using ICSharpCode.NRefactory.TypeSystem; using ICSharpCode.NRefactory.TypeSystem.Implementation; using System.Text; using System.Collections.ObjectModel; namespace MonoDevelop.Ide.Gui { public class Document : ICSharpCode.NRefactory.AbstractAnnotatable { internal object MemoryProbe = Counters.DocumentsInMemory.CreateMemoryProbe (); IWorkbenchWindow window; TextEditorExtension editorExtension; ParsedDocument parsedDocument; IProjectContent singleFileContext; Mono.TextEditor.ITextEditorDataProvider provider = null; const int ParseDelay = 600; public IWorkbenchWindow Window { get { return window; } } internal DateTime LastTimeActive { get; set; } public TextEditorExtension EditorExtension { get { return this.editorExtension; } } public T GetContent () where T : class { if (window == null) return null; //check whether the ViewContent can return the type directly T ret = Window.ActiveViewContent.GetContent (typeof(T)) as T; if (ret != null) return ret; //check the primary viewcontent //not sure if this is the right thing to do, but things depend on this behaviour if (Window.ViewContent != Window.ActiveViewContent) { ret = Window.ViewContent.GetContent (typeof(T)) as T; if (ret != null) return ret; } //no, so look through the TexteditorExtensions as well TextEditorExtension nextExtension = editorExtension; while (nextExtension != null) { if (typeof(T).IsAssignableFrom (nextExtension.GetType ())) return nextExtension as T; nextExtension = nextExtension.Next as TextEditorExtension; } return null; } public IEnumerable GetContents () where T : class { //check whether the ViewContent can return the type directly T ret = (T) Window.ActiveViewContent.GetContent (typeof(T)); if (ret != null) yield return ret; //check the primary viewcontent //not sure if this is the right thing to do, but things depend on this behaviour if (Window.ViewContent != Window.ActiveViewContent) { ret = (T) Window.ViewContent.GetContent (typeof(T)); if (ret != null) yield return ret; } //no, so look through the TexteditorExtensions as well TextEditorExtension nextExtension = editorExtension; while (nextExtension != null) { if (typeof(T).IsAssignableFrom (nextExtension.GetType ())) yield return nextExtension as T; nextExtension = nextExtension.Next as TextEditorExtension; } } static Document () { if (IdeApp.Workbench != null) { IdeApp.Workbench.ActiveDocumentChanged += delegate { // reparse on document switch to update the current file with changes done in other files. var doc = IdeApp.Workbench.ActiveDocument; if (doc == null || doc.Editor == null) return; doc.StartReparseThread (); }; } } public Document (IWorkbenchWindow window) { Counters.OpenDocuments++; LastTimeActive = DateTime.Now; this.window = window; window.Closed += OnClosed; window.ActiveViewContentChanged += OnActiveViewContentChanged; if (IdeApp.Workspace != null) IdeApp.Workspace.ItemRemovedFromSolution += OnEntryRemoved; if (window.ViewContent.Project != null) window.ViewContent.Project.Modified += HandleProjectModified; window.ViewsChanged += HandleViewsChanged; } /* void UpdateRegisteredDom (object sender, ProjectDomEventArgs e) { if (dom == null || dom.Project == null) return; var project = e.ITypeResolveContext != null ? e.ITypeResolveContext.Project : null; if (project != null && project.FileName == dom.Project.FileName) dom = e.ITypeResolveContext; }*/ public FilePath FileName { get { if (Window == null || !Window.ViewContent.IsFile) return null; return Window.ViewContent.IsUntitled ? Window.ViewContent.UntitledName : Window.ViewContent.ContentName; } } public bool IsFile { get { return Window.ViewContent.IsFile; } } public bool IsDirty { get { return !Window.ViewContent.IsViewOnly && (Window.ViewContent.ContentName == null || Window.ViewContent.IsDirty); } set { Window.ViewContent.IsDirty = value; } } public bool HasProject { get { return Window != null ? Window.ViewContent.Project != null : false; } } public Project Project { get { return Window != null ? Window.ViewContent.Project : null; } /* set { Window.ViewContent.Project = value; if (value != null) singleFileContext = null; // File needs to be in sync with the project, otherwise the parsed document at start may be invalid. // better solution: create the document with the project attached. StartReparseThread (); }*/ } public bool IsCompileableInProject { get { var project = Project; if (project == null) return false; var solution = project.ParentSolution; if (solution != null && IdeApp.Workspace != null) { var config = IdeApp.Workspace.ActiveConfiguration; if (config != null) { var sc = solution.GetConfiguration (config); if (sc != null && !sc.BuildEnabledForItem (project)) return false; } } var pf = project.GetProjectFile (FileName); return pf != null && pf.BuildAction != BuildAction.None; } } public IProjectContent ProjectContent { get { return Project != null ? TypeSystemService.GetProjectContext (Project) : GetProjectContext (); } } public virtual ICompilation Compilation { get { return Project != null ? TypeSystemService.GetCompilation (Project) : GetProjectContext ().CreateCompilation (); } } public virtual ParsedDocument ParsedDocument { get { return parsedDocument; } } public string PathRelativeToProject { get { return Window.ViewContent.PathRelativeToProject; } } public void Select () { window.SelectWindow (); } public DocumentView ActiveView { get { LoadViews (true); return WrapView (window.ActiveViewContent); } } public DocumentView PrimaryView { get { LoadViews (true); return WrapView (window.ViewContent); } } public ReadOnlyCollection Views { get { LoadViews (true); if (viewsRO == null) viewsRO = new ReadOnlyCollection (views); return viewsRO; } } ReadOnlyCollection viewsRO; List views = new List (); void HandleViewsChanged (object sender, EventArgs e) { LoadViews (false); } void LoadViews (bool force) { if (!force && views == null) return; var newList = new List (); newList.Add (WrapView (window.ViewContent)); foreach (var v in window.SubViewContents) newList.Add (WrapView (v)); views = newList; viewsRO = null; } DocumentView WrapView (IBaseViewContent content) { if (content == null) return null; if (views != null) return views.FirstOrDefault (v => v.BaseContent == content) ?? new DocumentView (this, content); else return new DocumentView (this, content); } public string Name { get { IViewContent view = Window.ViewContent; return view.IsUntitled ? view.UntitledName : view.ContentName; } } public Mono.TextEditor.TextEditorData Editor { get { if (provider == null) { provider = GetContent (); if (provider == null) return null; } return provider.GetTextEditorData (); } } public bool IsViewOnly { get { return Window.ViewContent.IsViewOnly; } } public void Reload () { ICustomXmlSerializer memento = null; IMementoCapable mc = GetContent (); if (mc != null) { memento = mc.Memento; } window.ViewContent.Load (window.ViewContent.ContentName); if (memento != null) { mc.Memento = memento; } } public void Save () { // suspend type service "check all file loop" since we have already a parsed document. // Or at least one that updates "soon". TypeSystemService.TrackFileChanges = false; try { if (Window.ViewContent.IsViewOnly || !Window.ViewContent.IsDirty) return; if (!Window.ViewContent.IsFile) { Window.ViewContent.Save (); return; } if (Window.ViewContent.ContentName == null) { SaveAs (); } else { try { FileService.RequestFileEdit (Window.ViewContent.ContentName, true); } catch (Exception ex) { MessageService.ShowException (ex, GettextCatalog.GetString ("The file could not be saved.")); } FileAttributes attr = FileAttributes.ReadOnly | FileAttributes.Directory | FileAttributes.Offline | FileAttributes.System; if (!File.Exists (Window.ViewContent.ContentName) || (File.GetAttributes (window.ViewContent.ContentName) & attr) != 0) { SaveAs (); } else { string fileName = Window.ViewContent.ContentName; // save backup first if ((bool)PropertyService.Get ("SharpDevelop.CreateBackupCopy", false)) { Window.ViewContent.Save (fileName + "~"); FileService.NotifyFileChanged (fileName); } Window.ViewContent.Save (fileName); FileService.NotifyFileChanged (fileName); OnSaved (EventArgs.Empty); } } } finally { // Set the file time of the current document after the file time of the written file, to prevent double file updates. // Note that the parsed document may be overwritten by a background thread to a more recent one. var doc = parsedDocument; if (doc != null && doc.ParsedFile != null) { string fileName = Window.ViewContent.ContentName; try { doc.ParsedFile.LastWriteTime = File.GetLastWriteTimeUtc (fileName); } catch (Exception e) { doc.ParsedFile.LastWriteTime = DateTime.UtcNow; LoggingService.LogWarning ("Exception while getting the write time from " + fileName, e); } } TypeSystemService.TrackFileChanges = true; } } public void SaveAs () { SaveAs (null); } public void SaveAs (string filename) { if (Window.ViewContent.IsViewOnly || !Window.ViewContent.IsFile) return; Encoding encoding = null; IEncodedTextContent tbuffer = GetContent (); if (tbuffer != null) { encoding = tbuffer.SourceEncoding; if (encoding == null) encoding = Encoding.Default; } if (filename == null) { var dlg = new OpenFileDialog (GettextCatalog.GetString ("Save as..."), FileChooserAction.Save) { TransientFor = IdeApp.Workbench.RootWindow, Encoding = encoding, ShowEncodingSelector = (tbuffer != null), }; if (Window.ViewContent.IsUntitled) dlg.InitialFileName = Window.ViewContent.UntitledName; else { dlg.CurrentFolder = Path.GetDirectoryName (Window.ViewContent.ContentName); dlg.InitialFileName = Path.GetFileName (Window.ViewContent.ContentName); } if (!dlg.Run ()) return; filename = dlg.SelectedFile; encoding = dlg.Encoding; } if (!FileService.IsValidPath (filename)) { MessageService.ShowMessage (GettextCatalog.GetString ("File name {0} is invalid", filename)); return; } // detect preexisting file if (File.Exists (filename)) { if (!MessageService.Confirm (GettextCatalog.GetString ("File {0} already exists. Overwrite?", filename), AlertButton.OverwriteFile)) return; } // save backup first if ((bool)PropertyService.Get ("SharpDevelop.CreateBackupCopy", false)) { if (tbuffer != null && encoding != null) tbuffer.Save (filename + "~", encoding); else Window.ViewContent.Save (filename + "~"); } TypeSystemService.RemoveSkippedfile (FileName); // do actual save if (tbuffer != null && encoding != null) tbuffer.Save (filename, encoding); else Window.ViewContent.Save (filename); FileService.NotifyFileChanged (filename); DesktopService.RecentFiles.AddFile (filename, (Project)null); OnSaved (EventArgs.Empty); UpdateParseDocument (); } public bool Close () { return ((SdiWorkspaceWindow)Window).CloseWindow (false, true); } void OnSaved (EventArgs args) { IdeApp.Workbench.SaveFileStatus (); if (Saved != null) Saved (this, args); } public void CancelParseTimeout () { if (parseTimeout != 0) { GLib.Source.Remove (parseTimeout); parseTimeout = 0; } } bool isClosed; void OnClosed (object s, EventArgs a) { isClosed = true; // TypeSystemService.DomRegistered -= UpdateRegisteredDom; CancelParseTimeout (); ClearTasks (); TypeSystemService.RemoveSkippedfile (FileName); try { OnClosed (a); } catch (Exception ex) { LoggingService.LogError ("Exception while calling OnClosed.", ex); } // Parse the file when the document is closed. In this way if the document // is closed without saving the changes, the saved compilation unit // information will be restored /* if (currentParseFile != null) { TypeSystemService.QueueParseJob (dom, delegate (string name, IProgressMonitor monitor) { TypeSystemService.Parse (curentParseProject, currentParseFile); }, FileName); } if (isFileDom) { TypeSystemService.RemoveFileDom (FileName); dom = null; }*/ Counters.OpenDocuments--; } internal void DisposeDocument () { DetachExtensionChain (); RemoveAnnotations (typeof(System.Object)); if (window is SdiWorkspaceWindow) ((SdiWorkspaceWindow)window).DetachFromPathedDocument (); window.Closed -= OnClosed; window.ActiveViewContentChanged -= OnActiveViewContentChanged; if (IdeApp.Workspace != null) IdeApp.Workspace.ItemRemovedFromSolution -= OnEntryRemoved; // Unsubscribe project events if (window.ViewContent.Project != null) window.ViewContent.Project.Modified -= HandleProjectModified; window.ViewsChanged += HandleViewsChanged; window = null; parsedDocument = null; singleFileContext = null; provider = null; views = null; viewsRO = null; } #region document tasks object lockObj = new object (); void ClearTasks () { lock (lockObj) { TaskService.Errors.ClearByOwner (this); } } // void CompilationUnitUpdated (object sender, ParsedDocumentEventArgs args) // { // if (this.FileName == args.FileName) { //// if (!args.Unit.HasErrors) // parsedDocument = args.ParsedDocument; ///* TODO: Implement better task update algorithm. // // ClearTasks (); // lock (lockObj) { // foreach (Error error in args.Unit.Errors) { // tasks.Add (new Task (this.FileName, error.Message, error.Column, error.Line, error.ErrorType == ErrorType.Error ? TaskType.Error : TaskType.Warning, this.Project)); // } // IdeApp.Services.TaskService.AddRange (tasks); // }*/ // } // } #endregion void OnActiveViewContentChanged (object s, EventArgs args) { OnViewChanged (args); } void OnClosed (EventArgs args) { if (Closed != null) Closed (this, args); } void OnViewChanged (EventArgs args) { if (ViewChanged != null) ViewChanged (this, args); } bool wasEdited; void InitializeExtensionChain () { DetachExtensionChain (); var editor = GetContent (); ExtensionNodeList extensions = window.ExtensionContext.GetExtensionNodes ("/MonoDevelop/Ide/TextEditorExtensions", typeof(TextEditorExtensionNode)); editorExtension = null; TextEditorExtension last = null; var mimetypeChain = DesktopService.GetMimeTypeInheritanceChainForFile (FileName).ToArray (); foreach (TextEditorExtensionNode extNode in extensions) { if (!extNode.Supports (FileName, mimetypeChain)) continue; TextEditorExtension ext; try { ext = (TextEditorExtension)extNode.CreateInstance (); } catch (Exception e) { LoggingService.LogError ("Error while creating text editor extension :" + extNode.Id + "(" + extNode.Type +")", e); continue; } if (ext.ExtendsEditor (this, editor)) { if (last != null) { ext.Next = last.Next; last.Next = ext; last = ext; } else { editorExtension = last = ext; last.Next = editor.AttachExtension (editorExtension); } ext.Initialize (this); } } if (window is SdiWorkspaceWindow) ((SdiWorkspaceWindow)window).AttachToPathedDocument (GetContent ()); } void DetachExtensionChain () { while (editorExtension != null) { try { editorExtension.Dispose (); } catch (Exception ex) { LoggingService.LogError ("Exception while disposing extension:" + editorExtension, ex); } editorExtension = editorExtension.Next as TextEditorExtension; } editorExtension = null; } void InitializeEditor (IExtensibleTextEditor editor) { Editor.Document.TextReplaced += (o, a) => { if (parsedDocument != null) parsedDocument.IsInvalid = true; if (Editor.Document.IsInAtomicUndo) { wasEdited = true; } else { StartReparseThread (); } }; Editor.Document.BeginUndo += delegate { wasEdited = false; }; Editor.Document.EndUndo += delegate { if (wasEdited) StartReparseThread (); }; Editor.Document.Undone += (o, a) => StartReparseThread (); Editor.Document.Redone += (o, a) => StartReparseThread (); InitializeExtensionChain (); } internal void OnDocumentAttached () { IExtensibleTextEditor editor = GetContent (); if (editor != null) { InitializeEditor (editor); RunWhenLoaded (delegate { ListenToProjectLoad (Project); }); } window.Document = this; } /// /// Performs an action when the content is loaded. /// /// /// The action to run. /// public void RunWhenLoaded (System.Action action) { var e = Editor; if (e == null || e.Document == null) { action (); return; } e.Document.RunWhenLoaded (action); } public void AttachToProject (Project project) { SetProject (project); } TypeSystemService.ProjectContentWrapper currentWrapper; internal void SetProject (Project project) { if (Window == null || Window.ViewContent == null || Window.ViewContent.Project == project) return; DetachExtensionChain (); ISupportsProjectReload pr = GetContent (); if (pr != null) { // Unsubscribe project events if (Window.ViewContent.Project != null) Window.ViewContent.Project.Modified -= HandleProjectModified; Window.ViewContent.Project = project; pr.Update (project); } if (project != null) project.Modified += HandleProjectModified; InitializeExtensionChain (); ListenToProjectLoad (project); } void ListenToProjectLoad (Project project) { if (currentWrapper != null) { currentWrapper.Loaded -= HandleInLoadChanged; currentWrapper = null; } if (project != null) { var wrapper = TypeSystemService.GetProjectContentWrapper (project); wrapper.Loaded += HandleInLoadChanged; currentWrapper = wrapper; currentWrapper.RequestLoad (); } StartReparseThread (); } void HandleInLoadChanged (object sender, EventArgs e) { StartReparseThread (); } void HandleProjectModified (object sender, SolutionItemModifiedEventArgs e) { if (!e.Any (x => x.Hint == "TargetFramework" || x.Hint == "References")) return; StartReparseThread (); } /// /// This method can take some time to finish. It's not threaded /// /// /// A that contains the current dom. /// public ParsedDocument UpdateParseDocument () { try { string currentParseFile = FileName; var editor = Editor; if (editor == null || string.IsNullOrEmpty (currentParseFile)) return null; TypeSystemService.AddSkippedFile (currentParseFile); string currentParseText = editor.Text; this.parsedDocument = TypeSystemService.ParseFile (Project, currentParseFile, editor.Document.MimeType, currentParseText); if (Project == null && this.parsedDocument != null) { singleFileContext = GetProjectContext ().AddOrUpdateFiles (parsedDocument.ParsedFile); } } finally { OnDocumentParsed (EventArgs.Empty); } return this.parsedDocument; } static readonly Lazy mscorlib = new Lazy ( () => new IkvmLoader ().LoadAssemblyFile (typeof (object).Assembly.Location)); static readonly Lazy systemCore = new Lazy( () => new IkvmLoader ().LoadAssemblyFile (typeof (System.Linq.Enumerable).Assembly.Location)); static readonly Lazy system = new Lazy( () => new IkvmLoader ().LoadAssemblyFile (typeof (System.Uri).Assembly.Location)); static IUnresolvedAssembly Mscorlib { get { return mscorlib.Value; } } static IUnresolvedAssembly SystemCore { get { return systemCore.Value; } } static IUnresolvedAssembly System { get { return system.Value; } } public bool IsProjectContextInUpdate { get { if (currentWrapper == null) return false; return !currentWrapper.IsLoaded; } } public virtual IProjectContent GetProjectContext () { if (Project == null) { if (singleFileContext == null) { singleFileContext = new ICSharpCode.NRefactory.CSharp.CSharpProjectContent (); singleFileContext = singleFileContext.AddAssemblyReferences (new [] { Mscorlib, System, SystemCore }); } if (parsedDocument != null) return singleFileContext.AddOrUpdateFiles (parsedDocument.ParsedFile); return singleFileContext; } return TypeSystemService.GetProjectContext (Project); } uint parseTimeout = 0; object reparseLock = new object(); internal void StartReparseThread () { lock (reparseLock) { if (currentWrapper != null) currentWrapper.EnsureReferencesAreLoaded (); // Don't directly parse the document because doing it at every key press is // very inefficient. Do it after a small delay instead, so several changes can // be parsed at the same time. string currentParseFile = FileName; if (string.IsNullOrEmpty (currentParseFile)) return; CancelParseTimeout (); if (IsProjectContextInUpdate) { return; } parseTimeout = GLib.Timeout.Add (ParseDelay, delegate { var editor = Editor; if (editor == null || IsProjectContextInUpdate) { parseTimeout = 0; return false; } string currentParseText = editor.Text; string mimeType = editor.Document.MimeType; ThreadPool.QueueUserWorkItem (delegate { if (IsProjectContextInUpdate) { return; } TypeSystemService.AddSkippedFile (currentParseFile); var currentParsedDocument = TypeSystemService.ParseFile (Project, currentParseFile, mimeType, currentParseText); Application.Invoke (delegate { // this may be called after the document has closed, in that case the OnDocumentParsed event shouldn't be invoked. if (isClosed) return; this.parsedDocument = currentParsedDocument; OnDocumentParsed (EventArgs.Empty); }); }); parseTimeout = 0; return false; }); } } /// /// This method kicks off an async document parser and should be used instead of /// unless you need the parsed document immediately. /// public void ReparseDocument () { StartReparseThread (); } internal object ExtendedCommandTargetChain { get { // Only go through the text editor chain, if the text editor is selected as subview if (Window != null && Window.ActiveViewContent == Window.ViewContent) return editorExtension; return null; } } void OnEntryRemoved (object sender, SolutionItemEventArgs args) { if (args.SolutionItem == window.ViewContent.Project) window.ViewContent.Project = null; } void OnDocumentParsed (EventArgs e) { EventHandler handler = this.DocumentParsed; if (handler != null) handler (this, e); } public event EventHandler Closed; public event EventHandler Saved; public event EventHandler ViewChanged; public event EventHandler DocumentParsed; public string[] CommentTags { get { if (IsFile) return GetCommentTags (FileName); else return null; } } public static string[] GetCommentTags (string fileName) { //Document doc = IdeApp.Workbench.ActiveDocument; string loadedMimeType = DesktopService.GetMimeTypeForUri (fileName); Mono.TextEditor.Highlighting.SyntaxMode mode = null; foreach (string mt in DesktopService.GetMimeTypeInheritanceChain (loadedMimeType)) { mode = Mono.TextEditor.Highlighting.SyntaxModeService.GetSyntaxMode (null, mt); if (mode != null) break; } if (mode == null) return null; List ctags; if (mode.Properties.TryGetValue ("LineComment", out ctags) && ctags.Count > 0) { return new string [] { ctags [0] }; } List tags = new List (); if (mode.Properties.TryGetValue ("BlockCommentStart", out ctags)) tags.Add (ctags [0]); if (mode.Properties.TryGetValue ("BlockCommentEnd", out ctags)) tags.Add (ctags [0]); if (tags.Count == 2) return tags.ToArray (); else return null; } // public MonoDevelop.Projects.CodeGeneration.CodeGenerator CreateCodeGenerator () // { // return MonoDevelop.Projects.CodeGeneration.CodeGenerator.CreateGenerator (Editor.Document.MimeType, // Editor.Options.TabsToSpaces, Editor.Options.TabSize, Editor.EolMarker); // } /// /// If the document shouldn't restore the settings after the load it can be disabled with this method. /// That is useful when opening a document and programmatically scrolling to a specified location. /// public void DisableAutoScroll () { Mono.TextEditor.Utils.FileSettingsStore.Remove (FileName); } } [Serializable] public sealed class DocumentEventArgs : EventArgs { public Document Document { get; set; } public DocumentEventArgs (Document document) { this.Document = document; } } }