public FileTailerViewModel([NotNull] ILogger logger,
            [NotNull] ISchedulerProvider schedulerProvider,
            [NotNull] IFileWatcher fileWatcher,
            [NotNull] ISelectionMonitor selectionMonitor, 
            [NotNull] IClipboardHandler clipboardHandler, 
            [NotNull] ISearchInfoCollection searchInfoCollection, 
            [NotNull] IInlineViewerFactory inlineViewerFactory, 
            [NotNull] ISetting<GeneralOptions> generalOptions,
            [NotNull] ISearchMetadataCollection searchMetadataCollection,
            [NotNull] SearchOptionsViewModel searchOptionsViewModel,
            [NotNull] SearchHints searchHints,
            IObjectProvider objectProvider)
        {
            if (logger == null) throw new ArgumentNullException(nameof(logger));
            if (schedulerProvider == null) throw new ArgumentNullException(nameof(schedulerProvider));
            if (fileWatcher == null) throw new ArgumentNullException(nameof(fileWatcher));
            if (selectionMonitor == null) throw new ArgumentNullException(nameof(selectionMonitor));
            if (clipboardHandler == null) throw new ArgumentNullException(nameof(clipboardHandler));
            if (searchInfoCollection == null) throw new ArgumentNullException(nameof(searchInfoCollection));
            if (inlineViewerFactory == null) throw new ArgumentNullException(nameof(inlineViewerFactory));
            if (generalOptions == null) throw new ArgumentNullException(nameof(generalOptions));
            if (searchMetadataCollection == null) throw new ArgumentNullException(nameof(searchMetadataCollection));
            if (searchOptionsViewModel == null) throw new ArgumentNullException(nameof(searchOptionsViewModel));
            if (searchHints == null) throw new ArgumentNullException(nameof(searchHints));

            _objectProvider = objectProvider;
            SelectionMonitor = selectionMonitor;
            SearchOptions = searchOptionsViewModel;
            SearchHints = searchHints;
            CopyToClipboardCommand = new Command(()=> clipboardHandler.WriteToClipboard(selectionMonitor.GetSelectedText()));
            SelectedItemsCount = selectionMonitor.Selected.Connect().QueryWhenChanged(collection=> collection.Count).ForBinding();
            SearchCollection = new SearchCollection(searchInfoCollection, schedulerProvider);

            UsingDarkTheme = generalOptions.Value
                    .ObserveOn(schedulerProvider.MainThread)
                    .Select(options => options.Theme== Theme.Dark)
                    .ForBinding();

            HighlightTail = generalOptions.Value
                .ObserveOn(schedulerProvider.MainThread)
                .Select(options => options.HighlightTail)
                .ForBinding();

            HighlightDuration = generalOptions.Value
                .ObserveOn(schedulerProvider.MainThread)
                .Select(options => new Duration(TimeSpan.FromSeconds(options.HighlightDuration)))
                .ForBinding();

            //An observable which acts as a scroll command
            var autoChanged = this.WhenValueChanged(vm => vm.AutoTail);
            var scroller = _userScrollRequested.CombineLatest(autoChanged, (user, auto) =>
                        {
                            var mode = AutoTail ? ScrollReason.Tail : ScrollReason.User;
                            return  new ScrollRequest(mode, user.PageSize, user.FirstIndex);
                        })
                        .ObserveOn(schedulerProvider.Background)
                        .DistinctUntilChanged();

            IsLoading = searchInfoCollection.All.Take(1).Select(_ => false).StartWith(true).ForBinding();

            //command to add the current search to the tail collection
            var searchInvoker = SearchHints.SearchRequested.Subscribe(request =>
            {
                searchInfoCollection.Add(request.Text, request.UseRegEx);
            });

            //User feedback to show file size
            FileSizeText = fileWatcher.Latest.Select(fn=>fn.Size)
                .Select(size => size.FormatWithAbbreviation())
                .DistinctUntilChanged()
                .ForBinding();

            //tailer is the main object used to tail, scroll and filter in a file
            var lineScroller = new LineScroller(SearchCollection.Latest.ObserveOn(schedulerProvider.Background), scroller);

            //load lines into observable collection
            var lineProxyFactory = new LineProxyFactory(new TextFormatter(searchMetadataCollection),new LineMatches(searchMetadataCollection));
            var loader = lineScroller.Lines.Connect()
                .Transform(x=>lineProxyFactory.Create(x), new ParallelisationOptions(ParallelType.Ordered,3))
                .Sort(SortExpressionComparer<LineProxy>.Ascending(proxy => proxy))
                .ObserveOn(schedulerProvider.MainThread)
                .Bind(out _data)
                .DisposeMany()
                .Subscribe(changes => logger.Info($"Rows changed. {changes.Adds} adds, {changes.Removes} removed"), 
                            ex => logger.Error(ex, "There is a problem with bind data"));
            
            //monitor matching lines and start index,
            Count = searchInfoCollection.All.Select(latest=>latest.Count).ForBinding();
            CountText = searchInfoCollection.All.Select(latest => $"{latest.Count.ToString("##,###")} lines").ForBinding();
            LatestCount = SearchCollection.Latest.Select(latest => latest.Count).ForBinding();
            
            //track first visible index
            var firstIndexMonitor = lineScroller.Lines.Connect()
                .Buffer(TimeSpan.FromMilliseconds(250)).FlattenBufferResult()
                .ToCollection()
                .Select(lines => lines.Count == 0 ? 0 : lines.Select(l => l.Index).Max() - lines.Count+1)
                .ObserveOn(schedulerProvider.MainThread)
                .Subscribe(first =>
                {
                    FirstIndex = first;
                });

            //Create objects required for inline viewing
            var isUserDefinedChanged = SearchCollection.WhenValueChanged(sc => sc.Selected)
                .Where(selected=> selected!=null)
                .Select(selected => selected.IsUserDefined)
                .DistinctUntilChanged()
                .Replay(1)
                .RefCount();
            
            var inlineViewerVisible = isUserDefinedChanged.CombineLatest(this.WhenValueChanged(vm => vm.ShowInline),
                                                            (userDefined, showInline) => userDefined && showInline);
            
            CanViewInline = isUserDefinedChanged.ForBinding();
            InlineViewerVisible = inlineViewerVisible.ForBinding();

            //return an empty line provider unless user is viewing inline - this saves needless trips to the file
            var inline = searchInfoCollection.All.CombineLatest(inlineViewerVisible, (index, ud) => ud ? index : new EmptyLineProvider());
            InlineViewer = inlineViewerFactory.Create(inline, this.WhenValueChanged(vm => vm.SelectedItem),lineProxyFactory);

            _cleanUp = new CompositeDisposable(lineScroller,
                loader,
                firstIndexMonitor,
                IsLoading,
                Count,
                LatestCount,
                FileSizeText,
                SelectedItemsCount,
                CanViewInline,
                InlineViewer,
                InlineViewerVisible,
                SearchCollection,
                searchInfoCollection,
                HighlightTail,
                UsingDarkTheme,
                searchHints,
                searchMetadataCollection,
                searchMetadataCollection,
                SelectionMonitor,
                SearchOptions,
                searchInvoker,
                _userScrollRequested.SetAsComplete());
        }
Example #2
0
        public TailViewModel([NotNull] ILogger logger,
            [NotNull] ISchedulerProvider schedulerProvider,
            [NotNull] IFileWatcher fileWatcher,
            [NotNull] ISelectionMonitor selectionMonitor, 
            [NotNull] IClipboardHandler clipboardHandler, 
            [NotNull] ISearchInfoCollection searchInfoCollection, 
            [NotNull] IInlineViewerFactory inlineViewerFactory, 
            [NotNull] ISetting<GeneralOptions> generalOptions,
            [NotNull] ISearchMetadataCollection searchMetadataCollection,
            [NotNull] IStateBucketService stateBucketService,
            [NotNull] SearchOptionsViewModel searchOptionsViewModel,
            [NotNull] ITailViewStateRestorer restorer,
            [NotNull] SearchHints searchHints,
            [NotNull] ITailViewStateControllerFactory tailViewStateControllerFactory,
            [NotNull] IThemeProvider themeProvider)
        {
         
            if (logger == null) throw new ArgumentNullException(nameof(logger));
            if (schedulerProvider == null) throw new ArgumentNullException(nameof(schedulerProvider));
            if (fileWatcher == null) throw new ArgumentNullException(nameof(fileWatcher));
            if (selectionMonitor == null) throw new ArgumentNullException(nameof(selectionMonitor));
            if (clipboardHandler == null) throw new ArgumentNullException(nameof(clipboardHandler));
            if (searchInfoCollection == null) throw new ArgumentNullException(nameof(searchInfoCollection));
            if (inlineViewerFactory == null) throw new ArgumentNullException(nameof(inlineViewerFactory));
            if (generalOptions == null) throw new ArgumentNullException(nameof(generalOptions));
            if (searchMetadataCollection == null) throw new ArgumentNullException(nameof(searchMetadataCollection));
            if (stateBucketService == null) throw new ArgumentNullException(nameof(stateBucketService));
            if (searchOptionsViewModel == null) throw new ArgumentNullException(nameof(searchOptionsViewModel));
            if (searchHints == null) throw new ArgumentNullException(nameof(searchHints));
            if (themeProvider == null) throw new ArgumentNullException(nameof(themeProvider));

            Name = fileWatcher.FullName;
            SelectionMonitor = selectionMonitor;
            SearchOptions = searchOptionsViewModel;
            SearchHints = searchHints;

            CopyToClipboardCommand = new Command(() => clipboardHandler.WriteToClipboard(selectionMonitor.GetSelectedText()));
            OpenFileCommand = new Command(() => Process.Start(fileWatcher.FullName));
            OpenFolderCommand = new Command(() => Process.Start(fileWatcher.Folder));
            CopyPathToClipboardCommand = new Command(() => clipboardHandler.WriteToClipboard(fileWatcher.FullName));
            UnClearCommand = new Command(fileWatcher.Reset);
            ClearCommand = new Command(fileWatcher.Clear);
            KeyAutoTail = new Command(() => { AutoTail = true; });

            SearchCollection = new SearchCollection(searchInfoCollection, schedulerProvider);
            SearchMetadataCollection = searchMetadataCollection;
            
            var horizonalScrollArgs = new ReplaySubject<TextScrollInfo>(1);
            HorizonalScrollChanged = args => horizonalScrollArgs.OnNext(args);
            
            _tailViewStateControllerFactory = tailViewStateControllerFactory;
            
            //Move these 2 highlight fields to a service as all views require them
            UsingDarkTheme = generalOptions.Value
                    .ObserveOn(schedulerProvider.MainThread)
                    .Select(options => options.Theme== Theme.Dark)
                    .ForBinding();

            HighlightTail = generalOptions.Value
                .ObserveOn(schedulerProvider.MainThread)
                .Select(options => options.HighlightTail)
                .ForBinding();

            //this deals with state when loading the system at start up and at shut-down
            _persister = new TailViewPersister(this, restorer);
            
            //An observable which acts as a scroll command
            var autoChanged = this.WhenValueChanged(vm => vm.AutoTail);
            var scroller = _userScrollRequested.CombineLatest(autoChanged, (user, auto) =>
                        {
                            var mode = AutoTail ? ScrollReason.Tail : ScrollReason.User;
                            return  new ScrollRequest(mode, user.PageSize, user.FirstIndex);
                        })
                        .Do(x=>logger.Info("Scrolling to {0}/{1}", x.FirstIndex,x.PageSize))
                        .DistinctUntilChanged();

            FileStatus = fileWatcher.Status.ForBinding();

            //command to add the current search to the tail collection
            var searchInvoker = SearchHints.SearchRequested.Subscribe(request => searchInfoCollection.Add(request.Text, request.UseRegEx));

            //User feedback to show file size
            FileSizeText = fileWatcher.Latest.Select(fn=>fn.Size)
                .Select(size => size.FormatWithAbbreviation())
                .DistinctUntilChanged()
                .ForBinding();

            //tailer is the main object used to tail, scroll and filter in a file
            var selectedProvider = SearchCollection.Latest.ObserveOn(schedulerProvider.Background);
            
            var lineScroller = new LineScroller(selectedProvider, scroller);
            
            MaximumChars = lineScroller.MaximumLines()
                            .ObserveOn(schedulerProvider.MainThread)
                            .ForBinding();

            //load lines into observable collection
            var lineProxyFactory = new LineProxyFactory(new TextFormatter(searchMetadataCollection), new LineMatches(searchMetadataCollection), horizonalScrollArgs.DistinctUntilChanged(), themeProvider);

            var loader = lineScroller.Lines.Connect()
                .LogChanges(logger, "Received")
                .Transform(lineProxyFactory.Create, new ParallelisationOptions(ParallelType.Ordered, 3))
                .LogChanges(logger, "Sorting")
                .Sort(SortExpressionComparer<LineProxy>.Ascending(proxy => proxy))
                .ObserveOn(schedulerProvider.MainThread)
                .Bind(out _data,100)
                .LogChanges(logger, "Bound")
                .DisposeMany()
                .LogErrors(logger)
                .Subscribe();
            
            //monitor matching lines and start index,
            Count = searchInfoCollection.All.Select(latest=>latest.Count).ForBinding();
            CountText = searchInfoCollection.All.Select(latest => $"{latest.Count.ToString("##,###")} lines").ForBinding();
            LatestCount = SearchCollection.Latest.Select(latest => latest.Count).ForBinding();

            ////track first visible index
            var firstIndexMonitor = lineScroller.Lines.Connect()
                .Buffer(TimeSpan.FromMilliseconds(25)).FlattenBufferResult()
                .ToCollection()
                .Select(lines => lines.Count == 0 ? 0 : lines.Select(l => l.Index).Max() - lines.Count + 1)
                .ObserveOn(schedulerProvider.MainThread)
                .Subscribe(first =>
                {
                    FirstIndex = first;
                });

            //Create objects required for inline viewing
            var isUserDefinedChanged = SearchCollection.WhenValueChanged(sc => sc.Selected)
                .Where(selected=> selected!=null)
                .Select(selected => selected.IsUserDefined)
                .DistinctUntilChanged()
                .Replay(1)
                .RefCount();

            var showInline = this.WhenValueChanged(vm => vm.ShowInline);
            var inlineViewerVisible = isUserDefinedChanged.CombineLatest(showInline, (userDefined, showInlne) => userDefined && showInlne);
            
            CanViewInline = isUserDefinedChanged.ForBinding();
            InlineViewerVisible = inlineViewerVisible.ForBinding();

            //return an empty line provider unless user is viewing inline - this saves needless trips to the file
            var inline = searchInfoCollection.All.CombineLatest(inlineViewerVisible, (index, ud) => ud ? index : new EmptyLineProvider());

            InlineViewer = inlineViewerFactory.Create(inline, this.WhenValueChanged(vm => vm.SelectedItem), searchMetadataCollection);

            _cleanUp = new CompositeDisposable(lineScroller,
                loader,
                firstIndexMonitor,
                FileStatus,
                Count,
                LatestCount,
                FileSizeText,
                CanViewInline,
                InlineViewer,
                InlineViewerVisible,
                SearchCollection,
                searchInfoCollection,
                HighlightTail,
                UsingDarkTheme,
                searchHints,
                searchMetadataCollection,
                SelectionMonitor,
                SearchOptions,
                searchInvoker,
                MaximumChars,
                _stateMonitor,
                horizonalScrollArgs.SetAsComplete(),
                _userScrollRequested.SetAsComplete());
        }