├── .editorconfig
├── exe
├── gtkscratch.settings
├── scratch.exe
├── gtkscratch.exe
└── gtkscratch.exe.config
├── .gitignore
├── normalize-line-endings
├── gtk-ui
├── app.config
├── make
├── .editorconfig
├── packages.config
├── SnippetWindow.cs
├── MainWindow.cs
├── Program.cs
├── LogView.cs
├── ScratchView.cs
├── Resources.Designer.cs
├── ScratchRootController.cs
├── Resources.resx
├── GtkScratch.csproj
├── ScratchScopes.cs
├── bin
│ └── Debug
│ │ └── dir
│ │ └── 0-globalconfig.txt
├── SearchWindow.cs
├── BookView.cs
├── ScratchLegacy.cs
├── Scratch.cs
└── ScratchConf.cs
├── scratchlog
├── app.config
├── Properties
│ └── AssemblyInfo.cs
├── packages.config
├── Program.cs
└── scratchlog.csproj
├── scratch.sln
└── README.md
/.editorconfig:
--------------------------------------------------------------------------------
1 | end_of_line = lf
2 |
--------------------------------------------------------------------------------
/exe/gtkscratch.settings:
--------------------------------------------------------------------------------
1 | info-font=Verdana
2 | text-font=Monospace, 11px
3 |
--------------------------------------------------------------------------------
/exe/scratch.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/barrkel/scratch/HEAD/exe/scratch.exe
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.user
2 | bin/
3 | obj/
4 | test/
5 | packages/
6 | Backup/
7 | .vs/
8 |
--------------------------------------------------------------------------------
/exe/gtkscratch.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/barrkel/scratch/HEAD/exe/gtkscratch.exe
--------------------------------------------------------------------------------
/normalize-line-endings:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | cd "$(dirname $0)"
4 |
5 | find | egrep '.*\.(cs|csproj|sln)$' | xargs dos2unix >/dev/null 2>/dev/null
6 |
--------------------------------------------------------------------------------
/gtk-ui/app.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/exe/gtkscratch.exe.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gtk-ui/make:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | gmcs -r:/usr/lib/cli/gtk-sharp-2.0/gtk-sharp.dll \
4 | -r:/usr/lib/cli/gdk-sharp-2.0/gdk-sharp.dll \
5 | -r:/usr/lib/cli/glib-sharp-2.0/glib-sharp.dll \
6 | -r:/usr/lib/cli/pango-sharp-2.0/pango-sharp.dll \
7 | -o gtkscratch.exe \
8 | *.cs || exit
9 |
10 | mono --runtime=v4.0 gtkscratch.exe .
11 |
--------------------------------------------------------------------------------
/gtk-ui/.editorconfig:
--------------------------------------------------------------------------------
1 | end_of_line = lf
2 |
3 | [*.{cs,vb}]
4 |
5 | end_of_line = lf
6 |
7 | # IDE0044: Add readonly modifier
8 | dotnet_style_readonly_field = false:suggestion
9 |
10 | # IDE0040: Add accessibility modifiers
11 | dotnet_style_require_accessibility_modifiers = omit_if_default:silent
12 |
13 | # IDE0010: Add missing cases
14 | dotnet_diagnostic.IDE0010.severity = suggestion
15 |
--------------------------------------------------------------------------------
/scratchlog/app.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/gtk-ui/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/gtk-ui/SnippetWindow.cs:
--------------------------------------------------------------------------------
1 | using Gtk;
2 | using Barrkel.ScratchPad;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | namespace Barrkel.GtkScratchPad
8 | {
9 | public class SnippetWindow : Window
10 | {
11 | TextView _textView;
12 |
13 | public SnippetWindow(ScratchScope settings) : base("Snippet")
14 | {
15 | Scope = settings;
16 | InitComponent();
17 | }
18 |
19 | public ScratchScope Scope { get; }
20 |
21 | private void InitComponent()
22 | {
23 | Resize(500, 500);
24 | _textView = new TextView
25 | {
26 | WrapMode = WrapMode.Word
27 | };
28 |
29 | Title= Scope.GetOrDefault("title", "Snippet");
30 |
31 | Gdk.Color backgroundColor = new Gdk.Color(255, 255, 200);
32 | if (Scope.TryLookup("snippet-color", out var colorSetting))
33 | Gdk.Color.Parse(colorSetting.StringValue, ref backgroundColor);
34 |
35 | var textFont = Pango.FontDescription.FromString(Scope.GetOrDefault("text-font", "Courier New"));
36 |
37 | _textView.ModifyBase(StateType.Normal, backgroundColor);
38 | _textView.ModifyFont(textFont);
39 | _textView.Editable = false;
40 |
41 | _textView.Buffer.Text = Scope.Lookup("text").StringValue;
42 |
43 | ScrolledWindow scrolledTextView = new ScrolledWindow();
44 | scrolledTextView.Add(_textView);
45 |
46 | Add(scrolledTextView);
47 |
48 | BorderWidth = 0;
49 | }
50 | }
51 | }
52 |
53 |
--------------------------------------------------------------------------------
/scratchlog/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("scratchlog")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("scratchlog")]
13 | [assembly: AssemblyCopyright("Copyright © 2013")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("277beb24-8ca3-4714-9dc6-c54e8496c57d")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/scratch.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio Version 16
3 | VisualStudioVersion = 16.0.29709.97
4 | MinimumVisualStudioVersion = 10.0.40219.1
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GtkScratch", "gtk-ui\GtkScratch.csproj", "{DFB9B734-12C6-4014-997D-5B087B1FD53F}"
6 | EndProject
7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "scratchlog", "scratchlog\scratchlog.csproj", "{39151939-5820-4E36-8A14-01454735BF8B}"
8 | EndProject
9 | Global
10 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
11 | Debug|Any CPU = Debug|Any CPU
12 | Release|Any CPU = Release|Any CPU
13 | EndGlobalSection
14 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
15 | {DFB9B734-12C6-4014-997D-5B087B1FD53F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
16 | {DFB9B734-12C6-4014-997D-5B087B1FD53F}.Debug|Any CPU.Build.0 = Debug|Any CPU
17 | {DFB9B734-12C6-4014-997D-5B087B1FD53F}.Release|Any CPU.ActiveCfg = Release|Any CPU
18 | {DFB9B734-12C6-4014-997D-5B087B1FD53F}.Release|Any CPU.Build.0 = Release|Any CPU
19 | {39151939-5820-4E36-8A14-01454735BF8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
20 | {39151939-5820-4E36-8A14-01454735BF8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
21 | {39151939-5820-4E36-8A14-01454735BF8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
22 | {39151939-5820-4E36-8A14-01454735BF8B}.Release|Any CPU.Build.0 = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(SolutionProperties) = preSolution
25 | HideSolutionNode = FALSE
26 | EndGlobalSection
27 | GlobalSection(ExtensibilityGlobals) = postSolution
28 | SolutionGuid = {FC0A6B91-5638-46B5-8115-744482FBF23F}
29 | EndGlobalSection
30 | EndGlobal
31 |
--------------------------------------------------------------------------------
/gtk-ui/MainWindow.cs:
--------------------------------------------------------------------------------
1 | using Gtk;
2 | using Barrkel.ScratchPad;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | namespace Barrkel.GtkScratchPad
8 | {
9 | public class MainWindow : Window
10 | {
11 | Notebook _notebook;
12 |
13 | public MainWindow(ScratchRootController rootController) : base(WindowType.Toplevel)
14 | {
15 | RootController = rootController;
16 | rootController.ExitHandler += (sender, e) => Destroy();
17 | InitComponent();
18 | }
19 |
20 | public ScratchRootController RootController { get; }
21 |
22 | private void InitComponent()
23 | {
24 | Title = RootController.RootScope.GetOrDefault("app-title", "GTK ScratchPad");
25 | Resize(600, 600);
26 |
27 | List views = new List();
28 | _notebook = new Notebook();
29 |
30 | foreach (var book in RootController.Root.Books)
31 | {
32 | ScratchBookController controller = RootController.GetControllerFor(book);
33 | BookView view = new BookView(book, controller, this);
34 | views.Add(view);
35 | _notebook.AppendPage(view, CreateTabLabel(book.Name));
36 | }
37 | var logView = new LogView(RootController.RootScope, this);
38 | _notebook.AppendPage(logView, CreateTabLabel("log"));
39 | Log.Handler = logView.AppendLine;
40 |
41 | Add(_notebook);
42 |
43 | Destroyed += (o, e) =>
44 | {
45 | // TODO: call EnsureSaved on the root controller instead
46 | foreach (var view in views)
47 | view.EnsureSaved();
48 | Application.Quit();
49 | };
50 | }
51 |
52 | private static Label CreateTabLabel(string title)
53 | {
54 | Label viewLabel = new Label { Text = title };
55 | viewLabel.SetPadding(10, 2);
56 | return viewLabel;
57 | }
58 | }
59 | }
60 |
61 |
--------------------------------------------------------------------------------
/scratchlog/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/gtk-ui/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Gtk;
3 | using Barrkel.ScratchPad;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.IO;
7 |
8 | namespace Barrkel.GtkScratchPad
9 | {
10 | static class Program
11 | {
12 | public static int Main(string[] args)
13 | {
14 | try
15 | {
16 | while (true)
17 | {
18 | int result = AppMain(args);
19 | if (result >= 0)
20 | return result;
21 | }
22 | }
23 | catch (Exception ex)
24 | {
25 | Console.Error.WriteLine("Error: {0}", ex.Message);
26 | Console.Error.WriteLine("Stack trace: {0}", ex.StackTrace);
27 | Console.ReadLine();
28 | return 1;
29 | }
30 | }
31 |
32 | private static void NormalizeLineEndings(DirectoryInfo root)
33 | {
34 | Console.WriteLine("Processing notes in {0}", root.FullName);
35 | foreach (string baseName in root.GetFiles("*.txt")
36 | .Union(root.GetFiles("*.log"))
37 | .Select(f => Path.ChangeExtension(f.FullName, null))
38 | .Distinct())
39 | {
40 | Console.WriteLine("Normalizing {0}", baseName);
41 | ScratchPage.NormalizeLineEndings(baseName);
42 | }
43 | foreach (DirectoryInfo child in root.GetDirectories())
44 | if (child.Name != "." && child.Name != "..")
45 | NormalizeLineEndings(child);
46 | }
47 |
48 | public static int AppMain(string[] argArray)
49 | {
50 | List args = new List(argArray);
51 | Options options = new Options(args);
52 |
53 | string[] stub = Array.Empty();
54 | Application.Init("GtkScratchPad", ref stub);
55 |
56 | if (args.Count != 1)
57 | {
58 | Console.WriteLine("Expected argument: storage directory");
59 | return 1;
60 | }
61 |
62 | if (options.NormalizeFiles)
63 | {
64 | NormalizeLineEndings(new DirectoryInfo(args[0]));
65 | }
66 |
67 | ScratchScope rootScope = ScratchScope.CreateRoot();
68 | rootScope.Load(LegacyLibrary.Instance);
69 | rootScope.Load(ScratchLib.Instance);
70 |
71 | ScratchRoot root = new ScratchRoot(options, args[0], rootScope);
72 | ScratchLib.Instance.LoadConfig(root);
73 |
74 | ScratchRootController rootController = new ScratchRootController(root);
75 |
76 | MainWindow window = new MainWindow(rootController);
77 | window.ShowAll();
78 |
79 | Application.Run();
80 |
81 | return rootController.ExitIntent == ExitIntent.Exit ? 0 : -1;
82 | }
83 | }
84 | }
85 |
86 |
--------------------------------------------------------------------------------
/gtk-ui/LogView.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using Gtk;
6 | using Barrkel.ScratchPad;
7 | using System.IO;
8 |
9 | namespace Barrkel.GtkScratchPad
10 | {
11 | public class LogView : Frame
12 | {
13 | static readonly int MaxLength = 100_000;
14 | static readonly int TrimLength = 50_000;
15 | TextView _textView;
16 | int _length;
17 |
18 | public LogView(ScratchScope settings, Window appWindow)
19 | {
20 | AppSettings = settings;
21 | AppWindow = appWindow;
22 | InitComponent();
23 | }
24 |
25 | public Window AppWindow { get; private set; }
26 | public ScratchScope AppSettings { get; private set; }
27 |
28 | private void InitComponent()
29 | {
30 | Gdk.Color grey = new Gdk.Color(0xF0, 0xF0, 0xF0);
31 | var textFontName = AppSettings.GetOrDefault("log-font", null);
32 | if (textFontName == null)
33 | textFontName = AppSettings.GetOrDefault("text-font", "Courier New");
34 | var textFont = Pango.FontDescription.FromString(textFontName);
35 |
36 | _textView = new MyTextView
37 | {
38 | WrapMode = WrapMode.Word
39 | };
40 | _textView.ModifyBase(StateType.Normal, grey);
41 | // _textView.Buffer.Changed += _text_TextChanged;
42 | // _textView.KeyDownEvent += _textView_KeyDownEvent;
43 | _textView.Editable = false;
44 | _textView.ModifyFont(textFont);
45 |
46 | ScrolledWindow scrolledTextView = new ScrolledWindow();
47 | scrolledTextView.Add(_textView);
48 |
49 | VBox outerVertical = new VBox();
50 | outerVertical.PackStart(scrolledTextView, true, true, 0);
51 |
52 | Add(outerVertical);
53 |
54 | BorderWidth = 5;
55 | }
56 |
57 | public void AppendLine(string text)
58 | {
59 | text += "\n";
60 | TextIter end = _textView.Buffer.GetIterAtOffset(_length);
61 | _length += text.Length;
62 | _textView.Buffer.Insert(ref end, text);
63 | if (_length > MaxLength)
64 | Trim();
65 | _textView.ScrollToIter(
66 | _textView.Buffer.GetIterAtOffset(_length), 0, false, 0, 0);
67 | }
68 |
69 | public void Trim()
70 | {
71 | string text = _textView.Buffer.Text;
72 | string[] lines = text.Split(new char[] { '\n' }, StringSplitOptions.None);
73 |
74 | int startLine = 0;
75 | int len = 0;
76 | for (int i = lines.Length - 1; i >= 0; --i)
77 | {
78 | string line = lines[i];
79 | len += line.Length;
80 | if (len > TrimLength)
81 | {
82 | startLine = i + 1;
83 | break;
84 | }
85 | }
86 | StringBuilder result = new StringBuilder();
87 | for (int i = startLine; i < lines.Length; ++i)
88 | result.AppendLine(lines[i]);
89 | _length = result.Length;
90 | _textView.Buffer.Text = result.ToString();
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/gtk-ui/ScratchView.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace Barrkel.ScratchPad
8 | {
9 | internal class PageViewState
10 | {
11 | // Exists for nicer references to the constructor.
12 | internal static PageViewState Create()
13 | {
14 | return new PageViewState();
15 | }
16 |
17 | public (int, int)? CurrentSelection { get; set; }
18 | public int? CurrentScrollPos { get; set; }
19 | // [start, end) delimits text inserted in a completion attempt
20 | public (int, int)? CurrentCompletion { get; set; }
21 | }
22 |
23 | public delegate IEnumerable<(string, T)> SearchFunc(string text);
24 |
25 | public interface IScratchBookView
26 | {
27 | // Get the book this view is for; the view only ever shows a single page from a book
28 | ScratchBook Book { get; }
29 |
30 | // Inserts text at CurrentPosition
31 | void InsertText(string text);
32 | // Delete text backwards from CurrentPosition
33 | void DeleteTextBackwards(int count);
34 | // Gets 0-based position in text
35 | int CurrentPosition { get; set; }
36 | // Get or set the bounds of selected text; first is cursor, second is bound.
37 | (int, int) Selection { get; set; }
38 |
39 | // 0-based position in text of first character on visible line at top of view.
40 | // Assigning will attempt to set the scroll position so that this character is at the top.
41 | int ScrollPos { get; set; }
42 | // Ensure 0-based position in text is visible by scrolling if necessary.
43 | void ScrollIntoView(int position);
44 |
45 | // 0-based index of current Page in Book; may be equal to Book.Pages.Count for new page.
46 | // Assigning does not move page to end, unlike JumpToPage.
47 | int CurrentPageIndex { get; set; }
48 | // Current text in editor, which may be ahead of model (lazy saves).
49 | string CurrentText { get; }
50 |
51 | string Clipboard { get; }
52 | string SelectedText { get; set; }
53 |
54 | // View should call InvokeAction with actionName every millis milliseconds
55 | void AddRepeatingTimer(int millis, string actionName);
56 |
57 | void AddNewPage();
58 |
59 | // Show a search dialog with text box, list box and ok/cancel buttons.
60 | // List box is populated from result of searchFunc applied to contents of text box.
61 | // Returns true if OK clicked and item in list box selected, with associated T in result.
62 | // Returns false if Cancel clicked or search otherwise cancelled (Esc).
63 | bool RunSearch(SearchFunc searchFunc, out T result);
64 |
65 | // Show an input dialog.
66 | bool GetInput(ScratchScope settings, out string value);
67 |
68 | // Show a non-modal snippet window.
69 | void LaunchSnippet(ScratchScope settings);
70 |
71 | // Navigate version history.
72 | bool Navigate(Func callback);
73 |
74 | // Before invoking cross-page navigation, call this.
75 | void EnsureSaved();
76 | // Jump to page by index in Book.
77 | void JumpToPage(int page);
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/scratchlog/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using Barrkel.ScratchPad;
6 | using System.IO;
7 |
8 | namespace ScratchLog
9 | {
10 | class Program
11 | {
12 | static int Main(string[] args)
13 | {
14 | if (args.Length != 1)
15 | {
16 | Console.WriteLine("usage: {0} ", Path.GetFileNameWithoutExtension(
17 | Environment.GetCommandLineArgs()[0]));
18 | Console.WriteLine("Output date-stamped log of modifications in order with titles at time of modification");
19 | return 1;
20 | }
21 | List argList = new List(args);
22 | Options options = new Options(argList);
23 | ScratchRoot root = new ScratchRoot(options, argList[0], NullScope.Instance);
24 |
25 | var updates = new List();
26 |
27 | foreach (ScratchBook book in root.Books)
28 | {
29 | foreach (ScratchPage page in book.Pages)
30 | {
31 | ScratchIterator iter = page.GetIterator();
32 | iter.MoveToEnd();
33 | do {
34 | updates.Add(new Update { Title = new StringReader(iter.Text).ReadLine(), Stamp = iter.Stamp });
35 | } while (iter.MovePrevious());
36 | }
37 | }
38 | Console.WriteLine("Gathered {0} updates", updates.Count);
39 |
40 | Update previous = null;
41 | Update finish = null;
42 | int updateCount = 0;
43 | foreach (var update in updates.OrderByDescending(x => x.Stamp).Where(x => x.Title != null))
44 | {
45 | if (previous == null)
46 | {
47 | previous = update;
48 | continue;
49 | }
50 |
51 | if (previous.Title == update.Title && (previous.Stamp - update.Stamp).TotalHours < 1)
52 | {
53 | // within the hour => probably the same task
54 | if (finish == null)
55 | {
56 | // this is the start of a range
57 | finish = previous;
58 | updateCount = 1;
59 | }
60 | else
61 | {
62 | ++updateCount;
63 | }
64 | }
65 | else
66 | {
67 | if (finish != null)
68 | {
69 | // we've come to the start of a range, and previous was the beginning
70 | TimeSpan duration = finish.Stamp - previous.Stamp;
71 | Console.WriteLine("{0} {1} ({2}) {3}", previous.Stamp, NiceDuration(duration), updateCount, previous.Title);
72 | }
73 | else
74 | {
75 | // different task that previous, and not part of range
76 | Console.WriteLine("{0} {1}", previous.Stamp, previous.Title);
77 | }
78 |
79 | updateCount = 0;
80 | finish = null;
81 | }
82 |
83 | previous = update;
84 | }
85 | if (finish != null)
86 | {
87 | // we've come to the start of a range, and previous was the beginning
88 | TimeSpan duration = finish.Stamp - previous.Stamp;
89 | Console.WriteLine("{0} {1} ({2}) {3}", previous.Stamp, NiceDuration(duration), updates, previous.Title);
90 | }
91 | else
92 | {
93 | // different task that previous, and not part of range
94 | Console.WriteLine("{0} {1}", previous.Stamp, previous.Title);
95 | }
96 |
97 |
98 | return 0;
99 | }
100 |
101 | static string NiceDuration(TimeSpan span)
102 | {
103 | if (span.TotalDays > 1)
104 | return string.Format("{0} days {1} hours", (int) span.TotalDays, span.Hours);
105 | if (span.TotalHours > 1)
106 | return string.Format("{0} hours {1} mins", (int) span.TotalHours, span.Minutes);
107 | return string.Format("{0} minutes", (int) span.TotalMinutes);
108 | }
109 |
110 | class Update
111 | {
112 | public DateTime Stamp { get; set; }
113 | public string Title { get; set; }
114 | }
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/gtk-ui/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace Barrkel.GtkScratchPad {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | public class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | public static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Barrkel.GtkScratchPad.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | public static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 |
63 | ///
64 | /// Looks up a localized string similar to .globalconfig Default Global Config
65 | ///
66 | ///# Do not modify this file since it may be overwritten without notice.
67 | ///
68 | ///# Reset this config with reset-config
69 | ///C-M-R = reset-config
70 | ///
71 | ///####################################################################################################
72 | ///# UI config
73 | ///####################################################################################################
74 | ///text-font = "Consolas, 9"
75 | ///info-font = "Verdana, 12"
76 | ///log-font = "Consolas, 8"
77 | ///
78 | ///get-input-number = { |current|
79 | /// result = nil
80 | /// whil [rest of string was truncated]";.
81 | ///
82 | public static string globalconfig {
83 | get {
84 | return ResourceManager.GetString("globalconfig", resourceCulture);
85 | }
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/gtk-ui/ScratchRootController.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Linq;
5 | using System.Collections.ObjectModel;
6 | using System.IO;
7 | using System.Text.RegularExpressions;
8 | using System.Reflection;
9 | using System.Collections;
10 |
11 | namespace Barrkel.ScratchPad
12 | {
13 | public enum ExitIntent
14 | {
15 | Exit,
16 | Restart
17 | }
18 |
19 | // Controller for behaviour. UI should receive this and send keystrokes and events to it, along with view callbacks.
20 | // The view should be updated via the callbacks.
21 | // Much of it is stringly typed for a dynamically bound future.
22 | public class ScratchRootController
23 | {
24 | Dictionary _controllerMap = new Dictionary();
25 |
26 | public ScratchRoot Root { get; }
27 |
28 | public Options Options => Root.Options;
29 |
30 | public ScratchScope RootScope { get; }
31 |
32 | public ExitIntent ExitIntent { get; private set; } = ExitIntent.Exit;
33 |
34 | public event EventHandler ExitHandler;
35 |
36 | public void Exit(ExitIntent intent)
37 | {
38 | ExitIntent = intent;
39 | ExitHandler(this, EventArgs.Empty);
40 | }
41 |
42 | public ScratchBookController GetControllerFor(ScratchBook book)
43 | {
44 | if (_controllerMap.TryGetValue(book, out var result))
45 | {
46 | return result;
47 | }
48 | result = new ScratchBookController(this, book);
49 | _controllerMap.Add(book, result);
50 | return result;
51 | }
52 |
53 | public ScratchRootController(ScratchRoot root)
54 | {
55 | Root = root;
56 | RootScope = (ScratchScope)root.RootScope;
57 | }
58 | }
59 |
60 | public class ScratchBookController
61 | {
62 | // TODO: bullet mode, somewhat like auto indent; make tabs do similar things
63 | // TODO: keyword jump from hotkey, to create cross-page linking
64 | // TODO: read-only generated pages that e.g. collect sigil lines
65 | // Canonical example: TODO: lines, or discussion items for people; ought to link back
66 | // TODO: search over log history
67 | // TODO: load key -> action bindings from a note
68 | // TODO: move top-level logic (e.g. jumping) to controller
69 | // TODO: lightweight scripting for composing new actions
70 |
71 | public ScratchBook Book { get; }
72 | public ScratchRootController RootController { get; }
73 | public ScratchScope Scope { get; }
74 |
75 | public ScratchBookController(ScratchRootController rootController, ScratchBook book)
76 | {
77 | Book = book;
78 | RootController = rootController;
79 | Scope = (ScratchScope)book.Scope;
80 | }
81 |
82 | public void ConnectView(IScratchBookView view)
83 | {
84 | view.AddRepeatingTimer(3000, "check-for-save");
85 | }
86 |
87 | public bool InformKeyStroke(IScratchBookView view, string keyName, bool ctrl, bool alt, bool shift)
88 | {
89 | string ctrlPrefix = ctrl ? "C-" : "";
90 | string altPrefix = alt ? "M-" : "";
91 | // Convention: self-printing keys have a single character. Exceptions are Return and Space.
92 | // Within this convention, don't use S- if shift was pressed to access the key. M-A is M-S-a, but M-A is emacs style.
93 | string shiftPrefix = (keyName.Length > 1 && shift) ? "S-" : "";
94 | string key = string.Concat(ctrlPrefix, altPrefix, shiftPrefix, keyName);
95 |
96 | ExecutionContext context = new ExecutionContext(this, view, Scope);
97 |
98 | if (Scope.GetOrDefault("debug-keys", false))
99 | Log.Out($"debug-keys: {key}");
100 |
101 | if (Scope.TryLookup(key, out var action))
102 | {
103 | try
104 | {
105 | action.Invoke(key, context, ScratchValue.EmptyList);
106 | }
107 | catch (Exception ex)
108 | {
109 | Log.Out(ex.Message);
110 | }
111 | return true;
112 | }
113 |
114 | return false;
115 | }
116 |
117 | public void InvokeAction(IScratchBookView view, string actionName, IList args)
118 | {
119 | ExecutionContext context = new ExecutionContext(this, view, Scope);
120 | if (Scope.TryLookup(actionName, out var action))
121 | action.Invoke(actionName, context, args);
122 | }
123 |
124 | public void InformEvent(IScratchBookView view, string eventName, IList args)
125 | {
126 | ExecutionContext context = new ExecutionContext(this, view, Scope);
127 | string eventMethod = $"on-{eventName}";
128 | if (Scope.TryLookup(eventMethod, out var action))
129 | action.Invoke(eventMethod, context, args);
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ScratchPad for Windows and Mono
2 |
3 | Simple keyboard-driven scratchpad application with a Gtk# UI that works
4 | in Windows on .NET and Linux and macOS using Mono.
5 |
6 | Designed with constant persistence and unlimited history logging for
7 | persistent undo across sessions.
8 |
9 | Uses text-based storage and edit log to minimize risk of data loss. Text can
10 | be recovered by replaying log in case the .txt is lost; if the .log alone is
11 | lost, the most current text should still be OK. If the two get out of sync
12 | (e.g. modifying the .txt file independently), the conflict is resolved by
13 | replaying the log and and making the diff to the current text the final edit.
14 |
15 | The diff algorithm that produces the edit log is naive and very simple, in
16 | the interests of reducing code complexity and eliminating third-party
17 | dependencies.
18 |
19 | Navigation is expected to be done with the keyboard:
20 |
21 | * Alt+Up/Down navigates history backwards and forwards.
22 | * Alt+Left/Right navigates pages backwards and forwards.
23 | * F12 for simple title search dialog
24 | * Pages sorted in most recently edited order
25 |
26 | Run the application with a directory argument. The default tab will use that
27 | directory for storage. Any subdirectories found will be used as names for
28 | additional tabs. All .txt files in directories will be added as pages; all
29 | .log files will be replayed to recreate any .txt files if missing.
30 |
31 | There's a simple configuration language, ScratchConf, for tweaking the UI
32 | and encoding macro actions.
33 |
34 | ## ScratchConf
35 |
36 | Config files are read from pages which start with .config or .globalconfig.
37 | Settings from .config are scoped to a tab (book), while .globalconfig is
38 | shared across all tabs.
39 |
40 | The configuration language is designed to have no execution up front so that
41 | startup stays fast. Only symbol bindings are allowed at the top level.
42 | However, anonymous functions can be defined and functions which are bound
43 | to the names of keys (using Emacs conventions for C- M- S- modifiers)
44 | will be executed when that key (combination) is pressed.
45 |
46 | Comments are line-oriented and introduced by # or //.
47 |
48 | The language has no arithmetic, but the default imported library has functions
49 | for add(), sub(), mul(), div(), eq(), ne(), lt(), gt() and so on.
50 | The only literals are strings, non-negative integers and anonymous functions.
51 | Identifiers may include '-'. Functions take parameters Ruby-style, with || inside {}.
52 |
53 | The language uses dynamic scope. Function activation records are pushed on a
54 | stack of symbol tables on function entry and popped on exit. This means that locals
55 | defined in a function will temporarily hide globals for called functions.
56 | This is similar to the behaviour of Emacs Lisp. Binding within nested anonymous
57 | functions work as downward funargs, but the scope is not closed over - the bindings
58 | are looked up in a new context if the nested function is stored and executed after
59 | its inclosing activation record is popped (i.e. the function that created it returned).
60 |
61 | There are two binding syntaxes inside function bodies, '=' and ':='.
62 | The difference is that '=' will redefine an existing binding in the scope it's found
63 | in if it already exists, while ':=' always creates a new binding.
64 | In effect, always use ':=' inside function bodies unless you're trying to change
65 | the value of a global.
66 |
67 | There are two 'global' scopes: the root scope, where .globalconfig configs are loaded,
68 | and '.config', where book scopes are loaded. The idea is you can customize settings
69 | on a per-book basis where this makes sense, depending on what the book is used for.
70 |
71 | ### Grammar
72 |
73 | ```
74 | =~ [a-zA-Z_-][a-z0-9_-]* ;
75 | =~ '[^']*'|"[^"]*" ;
76 | =~ [0-9]+ ;
77 |
78 | file ::= { setting } .
79 | setting ::= ( | ) '=' ( literal | ) ;
80 | literal ::= | | block | 'nil' ;
81 | block ::= '{' [paramList] exprList '}' ;
82 | paramList ::= '|' [ { ',' } ] '|' ;
83 | exprList = { expr } ;
84 | expr ::= if | orExpr | return | while | 'break' | 'continue' ;
85 | while ::= 'while' orExpr '{' exprList '}' ;
86 | // consider removing this ambiguity on the basis of semantics
87 | // control will not flow to the next line, so what if we eat it
88 | return ::= 'return' [ '(' orExpr ')' ] ;
89 | orExpr ::= andExpr { '||' andExpr } ;
90 | andExpr ::= factor { '&&' factor } ;
91 | factor ::= [ '!' ] ( callOrAssign | literal | '(' expr ')' ) ;
92 | callOrAssign ::=
93 | ( ['(' [ expr { ',' expr } ] ')']
94 | | '=' expr
95 | | ':=' expr
96 | |
97 | ) ;
98 | if ::=
99 | 'if' orExpr '{' exprList '}'
100 | [ 'else' (if | '{' exprList '}') ]
101 | ;
102 | ```
103 |
104 | ### Examples
105 |
106 | #### Set preferences for font, background color
107 | ```
108 | text-font = "Consolas, 9"
109 | info-font = "Verdana, 12"
110 | log-font = "Consolas, 8"
111 | background-color = '#d1efcf'
112 | ```
113 |
114 | #### Create a standalone read-only snippet window containing selected text when F1 is pressed
115 |
116 | ```
117 | get-input-text = { |current|
118 | # get-input is a simple (and ugly) builtin text input dialog
119 | get-input({
120 | init-text := current
121 | })
122 | }
123 |
124 | show-snippet = { |snippet-text|
125 | # launch-snippet is a builtin function to create and show a new window
126 | # with read-only text
127 | # Its argument is an anonymous function whose bindings are used to
128 | # configure the window.
129 | launch-snippet({
130 | text := snippet-text
131 | snippet-color := '#FFFFC0'
132 | })
133 | }
134 |
135 | F1 = {
136 | sel := get-view-selected-text()
137 | if sel && ne(sel, '') {
138 | title := get-input-text("Snippet")
139 | launch-snippet({
140 | text := sel
141 | snippet-color := '#FFFFC0'
142 | })
143 | }
144 | }
145 | ```
146 |
147 | #### Replace text within a selected block of text using dialogs for search and replacement
148 |
149 | ```
150 | replace-command = {
151 | sel := get-view-selected-text()
152 | if !sel || eq(sel, '') { return }
153 | foo := get-input-text('Regex')
154 | if !foo { return }
155 | dst := get-input-text('Replacement')
156 | if !dst { return }
157 | ensure-saved()
158 | set-view-selected-text(gsub(sel, foo, dst))
159 | }
160 | ```
161 |
162 |
--------------------------------------------------------------------------------
/gtk-ui/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 |
122 | bin\Debug\dir\0-globalconfig.txt;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;Windows-1252
123 |
124 |
--------------------------------------------------------------------------------
/gtk-ui/GtkScratch.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | AnyCPU
6 | 9.0.30729
7 | 2.0
8 | {DFB9B734-12C6-4014-997D-5B087B1FD53F}
9 | Exe
10 | Properties
11 | Barrkel.GtkScratchPad
12 | gtkscratch
13 | v4.7
14 | 512
15 |
16 |
17 |
18 |
19 | 3.5
20 | publish\
21 | true
22 | Disk
23 | false
24 | Foreground
25 | 7
26 | Days
27 | false
28 | false
29 | true
30 | 0
31 | 1.0.0.%2a
32 | false
33 | false
34 | true
35 |
36 |
37 |
38 |
39 |
40 | true
41 | full
42 | false
43 | bin\Debug\
44 | DEBUG;TRACE
45 | prompt
46 | 4
47 | x86
48 | false
49 | true
50 | false
51 |
52 |
53 | pdbonly
54 | true
55 | bin\Release\
56 | TRACE
57 | prompt
58 | 4
59 | false
60 |
61 |
62 |
63 |
64 |
65 |
66 | False
67 | ..\..\..\other\bulk\mono-2.10.9\lib\mono\gtk-sharp-2.0\atk-sharp.dll
68 |
69 |
70 |
71 | False
72 | ..\..\..\other\bulk\mono-2.10.9\lib\mono\gtk-sharp-2.0\glib-sharp.dll
73 |
74 |
75 | False
76 | ..\..\..\other\bulk\mono-2.10.9\lib\mono\gtk-sharp-2.0\gtk-sharp.dll
77 |
78 |
79 |
80 |
81 | 3.5
82 |
83 |
84 | 3.5
85 |
86 |
87 | 3.5
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | True
97 | True
98 | Resources.resx
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | False
118 | .NET Framework 3.5 SP1
119 | true
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | PublicResXFileCodeGenerator
129 | Resources.Designer.cs
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
140 |
141 |
142 |
143 |
150 |
--------------------------------------------------------------------------------
/gtk-ui/ScratchScopes.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Linq;
5 | using System.Collections.ObjectModel;
6 | using System.IO;
7 | using System.Text.RegularExpressions;
8 | using System.Reflection;
9 | using System.Collections;
10 |
11 | namespace Barrkel.ScratchPad
12 | {
13 | public interface IScratchLibrary : IEnumerable<(string, ScratchValue)>
14 | {
15 | bool TryLookup(string name, out ScratchValue result);
16 | string Name { get; }
17 | }
18 |
19 | public abstract class ScratchLibraryBase : IScratchLibrary
20 | {
21 | protected ScratchLibraryBase(string name)
22 | {
23 | Name = name;
24 | }
25 |
26 | protected Dictionary Bindings { get; } = new Dictionary();
27 |
28 | public string Name { get; }
29 |
30 | public IEnumerator<(string, ScratchValue)> GetEnumerator()
31 | {
32 | foreach (var entry in Bindings)
33 | yield return (entry.Key, entry.Value);
34 | }
35 |
36 | public bool TryLookup(string name, out ScratchValue result)
37 | {
38 | return Bindings.TryGetValue(name, out result);
39 | }
40 |
41 | IEnumerator IEnumerable.GetEnumerator()
42 | {
43 | return GetEnumerator();
44 | }
45 | }
46 |
47 | delegate void ScratchActionVoid(ExecutionContext context, IList args);
48 |
49 | public abstract class NativeLibrary : ScratchLibraryBase
50 | {
51 | protected NativeLibrary(string name) : base(name)
52 | {
53 | foreach (var member in GetType().GetMembers())
54 | {
55 | if (member.MemberType != MemberTypes.Method)
56 | continue;
57 | MethodInfo method = (MethodInfo)member;
58 | foreach (VariadicActionAttribute attr in Attribute.GetCustomAttributes(member, typeof(VariadicActionAttribute)))
59 | {
60 | ScratchAction action;
61 | try
62 | {
63 | if (method.ReturnType == typeof(void))
64 | {
65 | ScratchActionVoid voidAction =
66 | (ScratchActionVoid)Delegate.CreateDelegate(typeof(ScratchActionVoid), this, method, true);
67 | action = (context, args) =>
68 | {
69 | voidAction(context, args);
70 | return ScratchValue.Null;
71 | };
72 | }
73 | else
74 | {
75 | action = (ScratchAction)Delegate.CreateDelegate(typeof(ScratchAction), this, method, true);
76 | }
77 | Bindings.Add(attr.Name, new ScratchValue(action));
78 | }
79 | catch (Exception ex)
80 | {
81 | Log.Out($"Error binding {method.Name}: {ex.Message}");
82 | }
83 | }
84 | foreach (TypedActionAttribute attr in Attribute.GetCustomAttributes(member, typeof(TypedActionAttribute)))
85 | {
86 | ScratchAction action;
87 | try
88 | {
89 | if (method.ReturnType == typeof(void))
90 | {
91 | ScratchActionVoid voidAction =
92 | (ScratchActionVoid)Delegate.CreateDelegate(typeof(ScratchActionVoid), this, method, true);
93 | action = (context, args) =>
94 | {
95 | Validate(attr.Name, args, attr.ParamTypes);
96 | voidAction(context, args);
97 | return ScratchValue.Null;
98 | };
99 | }
100 | else
101 | {
102 | var innerAction = (ScratchAction)Delegate.CreateDelegate(typeof(ScratchAction), this, method, true);
103 | action = (context, args) =>
104 | {
105 | Validate(attr.Name, args, attr.ParamTypes);
106 | return innerAction(context, args);
107 | };
108 | }
109 | Bindings.Add(attr.Name, new ScratchValue(action));
110 | }
111 | catch (Exception ex)
112 | {
113 | Log.Out($"Error binding {method.Name}: {ex.Message}");
114 | }
115 | }
116 | }
117 | }
118 |
119 | protected void Bind(string name, ScratchValue value)
120 | {
121 | Bindings[name] = value;
122 | }
123 |
124 | protected void ValidateLength(string method, IList args, int length)
125 | {
126 | if (args.Count != length)
127 | throw new ArgumentException($"Expected {length} arguments to {method} but got {args.Count}");
128 | }
129 |
130 | protected void ValidateArgument(string method, IList args, int index, ScratchType paramType)
131 | {
132 | if (args[index].Type != paramType)
133 | throw new ArgumentException($"Expected arg {index + 1} to {method} to be {paramType} but got {args[index].Type}");
134 | }
135 |
136 | protected void ValidateArgument(string method, IList args, int index, ScratchType paramType,
137 | ScratchType paramType2)
138 | {
139 | if (args[index].Type != paramType && args[index].Type != paramType2)
140 | throw new ArgumentException($"Expected arg {index + 1} to {method} to be {paramType} but got {args[index].Type}");
141 | }
142 |
143 | protected void Validate(string method, IList args, IList paramTypes)
144 | {
145 | ValidateLength(method, args, paramTypes.Count);
146 | for (int i = 0; i < args.Count; ++i)
147 | ValidateArgument(method, args, i, paramTypes[i]);
148 | }
149 | }
150 |
151 | public class ScratchScope : IScratchLibrary, IScratchScope
152 | {
153 | Dictionary _bindings = new Dictionary();
154 |
155 | private ScratchScope()
156 | {
157 | Name = "root";
158 | }
159 |
160 | private ScratchScope(ScratchScope parent, string name)
161 | {
162 | Parent = parent;
163 | Name = name;
164 | }
165 |
166 | public static ScratchScope CreateRoot()
167 | {
168 | return new ScratchScope();
169 | }
170 |
171 | public string Name { get; }
172 |
173 | // Additively load bindings from enumerable source.
174 | public void Load(IScratchLibrary library)
175 | {
176 | foreach (var (name, value) in library)
177 | _bindings[name] = value;
178 | }
179 |
180 | public ScratchScope Parent { get; }
181 |
182 | public bool TryLookup(string name, out ScratchValue result)
183 | {
184 | for (ScratchScope scope = this; scope != null; scope = scope.Parent)
185 | if (scope._bindings.TryGetValue(name, out result))
186 | return true;
187 | result = null;
188 | return false;
189 | }
190 |
191 | public ScratchValue Lookup(string name)
192 | {
193 | if (TryLookup(name, out ScratchValue result))
194 | return result;
195 | throw new ArgumentException("name not found in scope: " + name);
196 | }
197 |
198 | // We model assignment as updating a binding, rather than updating a box referenced by the binding.
199 | public void Assign(string name, ScratchValue value)
200 | {
201 | for (ScratchScope scope = this; scope != null; scope = scope.Parent)
202 | if (scope.TryAssign(name, value))
203 | return;
204 | AssignLocal(name, value);
205 | }
206 |
207 | public void AssignLocal(string name, ScratchValue value)
208 | {
209 | _bindings[name] = value;
210 | }
211 |
212 | public bool TryAssign(string name, ScratchValue value)
213 | {
214 | if (_bindings.ContainsKey(name))
215 | {
216 | _bindings[name] = value;
217 | return true;
218 | }
219 | return false;
220 | }
221 |
222 | public IEnumerator<(string, ScratchValue)> GetEnumerator()
223 | {
224 | foreach (var entry in _bindings)
225 | yield return (entry.Key, entry.Value);
226 | }
227 |
228 | // These GetOrDefault overloads are for pulling out settings
229 |
230 | public string GetOrDefault(string name, string defaultValue)
231 | {
232 | if (TryLookup(name, out var result) && result.Type == ScratchType.String)
233 | return result.StringValue;
234 | return defaultValue;
235 | }
236 |
237 | public bool GetOrDefault(string name, bool defaultValue)
238 | {
239 | if (TryLookup(name, out var result))
240 | return result.Type != ScratchType.Null;
241 | return defaultValue;
242 | }
243 |
244 | public int GetOrDefault(string name, int defaultValue)
245 | {
246 | if (TryLookup(name, out var result) && result.Type == ScratchType.Int32)
247 | return result.Int32Value;
248 | return defaultValue;
249 | }
250 |
251 | IEnumerator IEnumerable.GetEnumerator()
252 | {
253 | return GetEnumerator();
254 | }
255 |
256 | IScratchScope IScratchScope.CreateChild(string name)
257 | {
258 | return CreateChild(name);
259 | }
260 |
261 | public ScratchScope CreateChild(string name)
262 | {
263 | return new ScratchScope(this, name);
264 | }
265 | }
266 |
267 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
268 | public class TypedActionAttribute : Attribute
269 | {
270 | public TypedActionAttribute(string name, params ScratchType[] paramTypes)
271 | {
272 | Name = name;
273 | ParamTypes = paramTypes;
274 | }
275 |
276 | public string Name { get; }
277 | public IList ParamTypes { get; }
278 | }
279 |
280 | [AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
281 | public class VariadicActionAttribute : Attribute
282 | {
283 | public VariadicActionAttribute(string name)
284 | {
285 | Name = name;
286 | }
287 |
288 | public string Name { get; }
289 | }
290 | }
291 |
--------------------------------------------------------------------------------
/gtk-ui/bin/Debug/dir/0-globalconfig.txt:
--------------------------------------------------------------------------------
1 | .globalconfig Default Global Config
2 |
3 | ####################################################################################################
4 | # Do not modify this file since it may be overwritten without notice.
5 | ####################################################################################################
6 |
7 | #---------------------------------------------------------------------------------------------------
8 | # Available Functions
9 | #---------------------------------------------------------------------------------------------------
10 |
11 | // add
12 | // add-indent(text, indent: string): string
13 | // add-new-page
14 | // autoindent-return
15 | // call-action - invoke action via search UI
16 | // char-at(string, int): string
17 | // complete
18 | // concat(...): string
19 | // debug-stack
20 | // div
21 | // dp
22 | // dump-scopes - Output all scope values to the log.
23 | // ensure-saved
24 | // enter-page
25 | // eq
26 | // escape-re
27 | // exec
28 | // exit-app
29 | // exit-page
30 | // format(string, ...): string
31 | // ge
32 | // get-clipboard(): string
33 | // get-cursor-text-re
34 | // get-input([closure]): string - get input, scope args { init-text }
35 | // get-line-end(text: string, pos: int): int
36 | // get-line-ident(string, position)
37 | // get-line-start(text: string, pos: int): int
38 | // get-page-count(): int
39 | // get-page-index(): int - get index of current page
40 | // get-page-title(int): string
41 | // get-string-from-to(string, int, int): string
42 | // get-view-pos(): int
43 | // get-view-selected-text(): string
44 | // get-view-text(): string
45 | // get-whitespace(text: string; pos, max: int): string - Get all whitespace from text[pos] up to text[max] or non-whitespace
46 | // goto-next-major-version
47 | // goto-next-version
48 | // goto-previous-major-version
49 | // goto-previous-version
50 | // gsub
51 | // gsub(text, regex, replacement: string): string
52 | // gt
53 | // incremental-search(source, prefilter: (regex -> regex), transform: (string -> string))
54 | // indent-block
55 | // index-of(haystack, needle[, start-index[, end-index]]): int
56 | // insert-date
57 | // insert-datetime
58 | // insert-text(...)
59 | // is-defined(symbol: string): bool
60 | // jump-to-page(int) - set page index and move page to end
61 | // launch-snippet
62 | // le
63 | // length(string): int
64 | // load-config
65 | // log
66 | // lt
67 | // match-re(text, regex: string): string
68 | // mod
69 | // mul
70 | // navigate-contents
71 | // navigate-sigil
72 | // navigate-title
73 | // navigate-todo
74 | // ne
75 | // occur
76 | // on-text-changed
77 | // open
78 | // reset-config
79 | // reset-indent(text: string): string
80 | // restart-app - Tear down UI, reload all books and recreate UI.
81 | // reverse-lines
82 | // scroll-pos-into-view(pos: int)
83 | // search-current-page(prefilter: string) - occur but on lines matching regex
84 | // set-page-index(int) - set current page
85 | // set-view-pos(int)
86 | // set-view-selected-text(text: string)
87 | // set-view-selection(from, to: int)
88 | // smart-paste
89 | // sort-lines
90 | // sub
91 | // substring(string; startIndex, length: int): string
92 | // to-int(string): int
93 | // to-str(int): string
94 | // transform-lines
95 | // unindent-block
96 |
97 | // Variable: set to non-zero to debug scope bindings.
98 | // debug-binding
99 |
100 | // Variable: set to non-null to debug key bindings.
101 | // debug-keys
102 |
103 | #-------------------------------------------------------------------------------
104 | # UI config
105 | #-------------------------------------------------------------------------------
106 |
107 | text-font = "Consolas, 9"
108 | info-font = "Verdana, 12"
109 | log-font = "Consolas, 8"
110 | app-title = "Barry's Scratchpad"
111 |
112 |
113 | #-------------------------------------------------------------------------------
114 | # Key bindings
115 | #-------------------------------------------------------------------------------
116 |
117 | # Reset this config with reset-config
118 | C-M-R = reset-config
119 |
120 | C-S-F4 = restart-app
121 |
122 | "C-M-?" = dump-scopes
123 |
124 | debug-keys = nil
125 |
126 | C-Up = goto-prev-para
127 | C-Down = goto-next-para
128 | "C-*" = repeat-selection
129 |
130 | F1 = create-snippet-from-selection
131 |
132 | M-S-Up = { goto-previous-major-version(300) }
133 | M-S-Down = { goto-next-major-version(300) }
134 |
135 | M-r = replace-command
136 |
137 | C-o = launch-url
138 |
139 | C-a = goto-sol
140 | C-e = goto-eol
141 |
142 | "C-!" = { insert-header('#', 100) }
143 | "C-@" = { insert-header('*', 90) }
144 | "C-#" = { insert-header('-', 80) }
145 |
146 | M-Return = jump-title-link
147 |
148 | M-Left = goto-previous-page
149 | M-PgUp = goto-previous-page
150 | M-Right = goto-next-page
151 | M-PgDn = goto-next-page
152 |
153 | M-x = call-action
154 |
155 | M-o = occur
156 |
157 | #-------------------------------------------------------------------------------
158 | # Commands
159 | #-------------------------------------------------------------------------------
160 |
161 | occur = {
162 | search-current-page('')
163 | }
164 |
165 | find-page-index = { |title|
166 | index := get-page-count()
167 | while ge(index, 0) {
168 | if eq(title, get-page-title(index)) {
169 | return index
170 | }
171 | index = sub(index, 1)
172 | }
173 | return nil
174 | }
175 |
176 | jump-to-title = { |title|
177 | index := find-page-index(title)
178 | if !index { return }
179 | jump-to-page(index)
180 | }
181 |
182 | jump-title-link = {
183 | line := get-current-line-text()
184 | sigil := index-of(line, '>>> ')
185 | if !sigil { return }
186 | title := get-string-from-to(line, add(sigil, 4), length(line))
187 | jump-to-title(title)
188 | }
189 |
190 | goto-next-page = {
191 | target := add(get-page-index(), 1)
192 | if eq(target, get-page-count()) {
193 | add-new-page()
194 | } else {
195 | set-page-index(target)
196 | }
197 | }
198 |
199 | goto-previous-page = {
200 | set-page-index(sub(get-page-index(), 1))
201 | }
202 |
203 | create-snippet-from-selection = {
204 | sel := get-view-selected-text()
205 | if sel && ne(sel, '') {
206 | title := get-input-text("Snippet")
207 | launch-snippet({
208 | text := sel
209 | snippet-color := '#FFFFC0'
210 | })
211 | }
212 | }
213 |
214 | launch-url = {
215 | value := get-cursor-text-re("\S+")
216 | if !value { return }
217 | if !is-url(value) { return }
218 | open(value)
219 | }
220 |
221 | repeat-selection = {
222 | sel := get-view-selected-text()
223 | if !sel || eq(sel, '') { return }
224 | count := get-input-number('repeat count')
225 | if !count || le(count, 0) { return }
226 | set-view-selected-text(str-n(sel, count))
227 | }
228 |
229 | goto-eol = {
230 | set-view-pos(
231 | get-line-end(
232 | get-view-text(),
233 | get-view-pos()))
234 | }
235 |
236 | goto-sol = {
237 | set-view-pos(
238 | get-line-start(
239 | get-view-text(),
240 | get-view-pos()))
241 | }
242 |
243 | cut-current-line-text = {
244 | text := get-view-text()
245 | pos := get-view-pos()
246 | sol := get-line-start(text, pos)
247 | eol = get-line-end(text, pos)
248 | if le(eol, sol) { return }
249 | set-view-selection(sol, eol)
250 | result := get-view-selected-text()
251 | set-view-selected-text('')
252 | result
253 | }
254 |
255 | insert-header = { |char, count|
256 | header := cut-current-line-text()
257 | line := str-n(char, count)
258 | insert-line(line)
259 | insert-line(format("{0} ", char))
260 | insert-line(line)
261 | goto-prev-line-end()
262 | goto-prev-line-end()
263 | if header {
264 | insert-text(header)
265 | }
266 | }
267 |
268 | goto-prev-para = {
269 | text := get-view-text()
270 | pos := get-view-pos()
271 | sol := get-line-start(text, pos)
272 | // go to previous line if we are at start of current line
273 | if eq(pos, sol) {
274 | pos := sub(pos, 1)
275 | }
276 | while gt(pos, 0) {
277 | pos := get-line-start(text, pos)
278 | eol := get-line-end(text, pos)
279 | if eq(pos, eol) {
280 | // blank line
281 | break
282 | }
283 | pos := sub(pos, 1)
284 | }
285 | if gt(pos, 0) {
286 | // set-view-pos(pos)
287 | set-view-selection(pos, add(pos, 1))
288 | scroll-pos-into-view(pos)
289 | }
290 | }
291 |
292 | goto-next-para = {
293 | text := get-view-text()
294 | eof := length(text)
295 | pos := get-view-pos()
296 | eol := get-line-end(text, pos)
297 | // go to next line if we are at end of current line
298 | if eq(pos, eol) {
299 | pos := add(pos, 1)
300 | }
301 | while lt(pos, eof) {
302 | pos := get-line-start(text, pos)
303 | eol := get-line-end(text, pos)
304 | if eq(pos, eol) {
305 | // blank line
306 | break
307 | }
308 | pos := add(eol, 1)
309 | }
310 | // set-view-pos(pos)
311 | set-view-selection(pos, add(pos, 1))
312 | scroll-pos-into-view(pos)
313 | }
314 |
315 | replace-command = {
316 | sel := get-view-selected-text()
317 | if !sel || eq(sel, '') { return }
318 | foo := get-input-text('Regex')
319 | if !foo { return }
320 | dst := get-input-text('Replacement')
321 | if !dst { return }
322 | ensure-saved()
323 | set-view-selected-text(gsub(sel, foo, dst))
324 | }
325 |
326 |
327 |
328 | #-------------------------------------------------------------------------------
329 | # Utility functions
330 | #-------------------------------------------------------------------------------
331 |
332 | NEWLINE = '
333 | '
334 |
335 | get-line-indent = { |text, pos|
336 | get-whitespace(text, get-line-start(text, pos), pos)
337 | }
338 |
339 | is-url = { |text|
340 | match-re(text, "^https?://\S+$")
341 | }
342 |
343 | str-n = { |text, count|
344 | result := ""
345 | while gt(count, 0) {
346 | result = concat(result, text)
347 | count = sub(count, 1)
348 | }
349 | result
350 | }
351 |
352 | insert-n = { |text, count|
353 | insert-text(str-n(text, count))
354 | }
355 |
356 | goto-prev-line-end = {
357 | text := get-view-text()
358 | line-start := get-line-start(text, get-view-pos())
359 | prev-line-end := sub(line-start, 1)
360 | if ge(prev-line-end, 0) {
361 | set-view-pos(prev-line-end)
362 | }
363 | }
364 |
365 | insert-line = { |text|
366 | insert-text(concat(text, NEWLINE))
367 | }
368 |
369 | get-current-line-text = {
370 | text := get-view-text()
371 | pos := get-view-pos()
372 | sol := get-line-start(text, pos)
373 | eol = get-line-end(text, pos)
374 | if le(eol, sol) { return }
375 |
376 | get-string-from-to(text, sol, eol)
377 | }
378 |
379 | get-input-number = { |current|
380 | result = nil
381 | while !result {
382 | current = get-input-text(current)
383 | if !current {
384 | return nil
385 | }
386 | result = to-int(current)
387 | }
388 | result
389 | }
390 |
391 | get-input-text = { |current|
392 | get-input({
393 | init-text := current
394 | })
395 | }
396 |
397 |
--------------------------------------------------------------------------------
/gtk-ui/SearchWindow.cs:
--------------------------------------------------------------------------------
1 | using Gtk;
2 | using Barrkel.ScratchPad;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | namespace Barrkel.GtkScratchPad
8 | {
9 | public enum ModalResult
10 | {
11 | None,
12 | OK,
13 | Cancel
14 | }
15 |
16 | public delegate IEnumerable<(string, T)> SearchFunc(string text);
17 |
18 | public class ModalWindow : Window, IDisposable
19 | {
20 | public ModalWindow(string title) : base(title)
21 | {
22 | }
23 |
24 | protected override void OnHidden()
25 | {
26 | base.OnHidden();
27 | if (ModalResult == ModalResult.None)
28 | ModalResult = ModalResult.Cancel;
29 | }
30 |
31 | public override void Dispose()
32 | {
33 | base.Destroy();
34 | base.Dispose();
35 | }
36 |
37 | public ModalResult ModalResult
38 | {
39 | get; protected set;
40 | }
41 |
42 | public ModalResult ShowModal(Window parent)
43 | {
44 | ModalResult = ModalResult.None;
45 | Modal = true;
46 | TransientFor = parent;
47 | ShowAll();
48 | while (ModalResult == ModalResult.None)
49 | Application.RunIteration(true);
50 | return ModalResult;
51 | }
52 |
53 | protected override bool OnKeyPressEvent(Gdk.EventKey evnt)
54 | {
55 | switch (evnt.Key)
56 | {
57 | case Gdk.Key.Escape:
58 | ModalResult = ModalResult.Cancel;
59 | return false;
60 |
61 | case Gdk.Key.Return:
62 | ModalResult = ModalResult.OK;
63 | return false;
64 |
65 | default:
66 | return base.OnKeyPressEvent(evnt);
67 | }
68 | }
69 | }
70 |
71 | public class InputModalWindow : ModalWindow
72 | {
73 | TextView _textView;
74 |
75 | public InputModalWindow(ScratchScope settings) : base("Input")
76 | {
77 | Scope = settings;
78 | InitComponent();
79 | }
80 |
81 | public ScratchScope Scope { get; }
82 |
83 | private void InitComponent()
84 | {
85 | Resize(500, 100);
86 | _textView = new TextView();
87 |
88 | Gdk.Color lightBlue = new Gdk.Color(207, 207, 239);
89 |
90 | var textFont = Pango.FontDescription.FromString(Scope.GetOrDefault("text-font", "Courier New"));
91 |
92 | _textView.ModifyBase(StateType.Normal, lightBlue);
93 | // _textView.Buffer.Changed += (s, e) => { UpdateSearchBox(); };
94 | // _textView.KeyPressEvent += _textView_KeyPressEvent;
95 | _textView.ModifyFont(textFont);
96 | string initText = Scope.GetOrDefault("init-text", "");
97 | if (!string.IsNullOrEmpty(initText))
98 | {
99 | _textView.Buffer.Text = initText;
100 | TextIter start = _textView.Buffer.GetIterAtOffset(0);
101 | TextIter end = _textView.Buffer.GetIterAtOffset(initText.Length);
102 | _textView.Buffer.SelectRange(start, end);
103 | }
104 |
105 |
106 | Add(_textView);
107 | BorderWidth = 5;
108 | }
109 |
110 | public static bool GetInput(Window parent, ScratchScope settings, out string result)
111 | {
112 | using (InputModalWindow window = new InputModalWindow(settings))
113 | {
114 | switch (window.ShowModal(parent))
115 | {
116 | case ModalResult.OK:
117 | result = window._textView.Buffer.Text;
118 | return true;
119 |
120 | default:
121 | result = null;
122 | return false;
123 | }
124 | }
125 | }
126 |
127 | private void _textView_KeyPressEvent(object o, KeyPressEventArgs args)
128 | {
129 | // throw new NotImplementedException();
130 | }
131 | }
132 |
133 | delegate IEnumerable<(string, object)> SearchFunc(string text);
134 |
135 | public class SearchWindow : ModalWindow
136 | {
137 | TextView _searchTextView;
138 | TreeView _searchResultsView;
139 | ListStore _searchResultsStore;
140 | TreeViewColumn _valueColumn;
141 | int _searchResultsStoreCount; // asinine results store
142 |
143 | SearchWindow(SearchFunc searchFunc, ScratchScope settings) : base("ScratchPad")
144 | {
145 | SearchFunc = searchFunc;
146 | AppSettings = settings;
147 | InitComponent();
148 | }
149 |
150 | static SearchFunc Polymorphize(SearchFunc generic)
151 | {
152 | return text => generic(text).Select(x => (x.Item1, (object)x.Item2));
153 | }
154 |
155 | public static bool RunSearch(Window parent, SearchFunc searchFunc, ScratchScope settings, out T result)
156 | {
157 | using (SearchWindow window = new SearchWindow(Polymorphize(searchFunc), settings))
158 | {
159 | switch (window.ShowModal(parent))
160 | {
161 | case ModalResult.OK:
162 | if (window.SelectedItem == null)
163 | {
164 | result = default;
165 | return false;
166 | }
167 | result = (T) window.SelectedItem.Value;
168 | return true;
169 |
170 | default:
171 | result = default;
172 | return false;
173 | }
174 | }
175 | }
176 |
177 | public ScratchScope AppSettings { get; private set; }
178 | SearchFunc SearchFunc { get; set; }
179 |
180 | private void InitComponent()
181 | {
182 | Resize(500, 500);
183 |
184 | var vbox = new VBox();
185 |
186 | Gdk.Color grey = new Gdk.Color(0xA0, 0xA0, 0xA0);
187 | Gdk.Color lightBlue = new Gdk.Color(207, 207, 239);
188 | var infoFont = Pango.FontDescription.FromString(AppSettings.GetOrDefault("info-font", "Verdana"));
189 | var textFont = Pango.FontDescription.FromString(AppSettings.GetOrDefault("text-font", "Courier New"));
190 |
191 | _searchTextView = new TextView();
192 | _searchTextView.ModifyBase(StateType.Normal, lightBlue);
193 | _searchTextView.Buffer.Changed += (s, e) => { UpdateSearchBox(); };
194 | _searchTextView.KeyPressEvent += _searchTextView_KeyPressEvent;
195 | _searchTextView.ModifyFont(textFont);
196 | _searchTextView.Buffer.Text = "";
197 |
198 |
199 | _searchResultsStore = new ListStore(typeof(TitleSearchResult));
200 |
201 | _searchResultsView = new TreeView(_searchResultsStore);
202 | var valueRenderer = new CellRendererText();
203 | _valueColumn = new TreeViewColumn
204 | {
205 | Title = "Value"
206 | };
207 | _valueColumn.PackStart(valueRenderer, true);
208 | _valueColumn.SetCellDataFunc(valueRenderer, (TreeViewColumn col, CellRenderer cell, TreeModel model, TreeIter iter) =>
209 | {
210 | TitleSearchResult item = (TitleSearchResult) model.GetValue(iter, 0);
211 | ((CellRendererText)cell).Text = item.Title;
212 | });
213 | _searchResultsView.AppendColumn(_valueColumn);
214 |
215 | _searchResultsView.ModifyBase(StateType.Normal, lightBlue);
216 | _searchResultsView.ButtonPressEvent += _searchResultsView_ButtonPressEvent;
217 | _searchResultsView.KeyPressEvent += _searchResultsView_KeyPressEvent;
218 |
219 | var scrolledResults = new ScrolledWindow();
220 | scrolledResults.Add(_searchResultsView);
221 |
222 | vbox.PackStart(_searchTextView, false, false, 0);
223 | vbox.PackStart(scrolledResults, true, true, 0);
224 |
225 | Add(vbox);
226 |
227 | BorderWidth = 5;
228 |
229 | UpdateSearchBox();
230 | }
231 |
232 | private void UpdateSearchBox()
233 | {
234 | _searchResultsStore.Clear();
235 | _searchResultsStoreCount = 0;
236 |
237 | foreach (var (title, v) in SearchFunc(_searchTextView.Buffer.Text))
238 | {
239 | _searchResultsStore.SetValue(_searchResultsStore.Append(), 0,
240 | new TitleSearchResult(title, _searchResultsStoreCount, v));
241 | ++_searchResultsStoreCount;
242 |
243 | if (_searchResultsStoreCount >= 100)
244 | break;
245 | }
246 | if (_searchResultsStoreCount == 1)
247 | SelectedIndex = 0;
248 | else
249 | SelectedIndex = -1;
250 | }
251 |
252 | public int SelectedIndex
253 | {
254 | get
255 | {
256 | TreeIter iter;
257 | if (_searchResultsView.Selection.GetSelected(out iter))
258 | return ((TitleSearchResult) _searchResultsStore.GetValue(iter, 0)).Index;
259 | return -1;
260 | }
261 | set
262 | {
263 | var selection = _searchResultsView.Selection;
264 | if (value < 0 || value >= _searchResultsStoreCount)
265 | {
266 | selection.UnselectAll();
267 | return;
268 | }
269 |
270 | TreeIter iter;
271 | if (!_searchResultsStore.IterNthChild(out iter, value))
272 | return;
273 |
274 | selection.SelectIter(iter);
275 | _searchResultsView.ScrollToCell(_searchResultsStore.GetPath(iter), null, false, 0, 0);
276 | }
277 | }
278 |
279 | internal TitleSearchResult SelectedItem
280 | {
281 | get
282 | {
283 | if (_searchResultsView.Selection.GetSelected(out TreeIter iter))
284 | return (TitleSearchResult)_searchResultsStore.GetValue(iter, 0);
285 | return null;
286 | }
287 | }
288 |
289 | [GLib.ConnectBefore]
290 | void _searchTextView_KeyPressEvent(object o, KeyPressEventArgs args)
291 | {
292 | int selectedIndex = SelectedIndex;
293 |
294 | switch (args.Event.Key)
295 | {
296 | case Gdk.Key.Return:
297 | if (_searchResultsStoreCount > 0)
298 | {
299 | if (selectedIndex <= 0)
300 | selectedIndex = 0;
301 | SelectedIndex = selectedIndex;
302 | }
303 | ModalResult = ModalResult.OK;
304 | args.RetVal = true;
305 | break;
306 |
307 | case Gdk.Key.Up:
308 | // FIXME: make these things scroll the selection into view
309 | args.RetVal = true;
310 | if (_searchResultsStoreCount > 0)
311 | {
312 | --selectedIndex;
313 | if (selectedIndex <= 0)
314 | selectedIndex = 0;
315 | SelectedIndex = selectedIndex;
316 | }
317 | break;
318 |
319 | case Gdk.Key.Down:
320 | args.RetVal = true;
321 | if (_searchResultsStoreCount > 0)
322 | {
323 | ++selectedIndex;
324 | if (selectedIndex >= _searchResultsStoreCount)
325 | selectedIndex = _searchResultsStoreCount - 1;
326 | SelectedIndex = selectedIndex;
327 | }
328 | break;
329 | }
330 | }
331 |
332 | private void _searchResultsView_KeyPressEvent(object o, KeyPressEventArgs args)
333 | {
334 | int selectedIndex = SelectedIndex;
335 | switch (args.Event.Key)
336 | {
337 | case Gdk.Key.Return:
338 | if (_searchResultsStoreCount > 0)
339 | {
340 | if (selectedIndex <= 0)
341 | selectedIndex = 0;
342 | SelectedIndex = selectedIndex;
343 | }
344 | ModalResult = ModalResult.OK;
345 | args.RetVal = true;
346 | break;
347 | }
348 | }
349 |
350 | [GLib.ConnectBefore]
351 | void _searchResultsView_ButtonPressEvent(object o, ButtonPressEventArgs args)
352 | {
353 | // consider handling double-click
354 | }
355 | }
356 |
357 | class TitleSearchResult
358 | {
359 | public TitleSearchResult(string title, int index, object value)
360 | {
361 | Title = title;
362 | Index = index;
363 | Value = value;
364 | }
365 |
366 | public string Title { get; private set; }
367 | public int Index { get; private set; }
368 | public object Value { get; private set; }
369 |
370 | public override string ToString()
371 | {
372 | return Title;
373 | }
374 | }
375 | }
376 |
377 |
--------------------------------------------------------------------------------
/scratchlog/scratchlog.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | 9.0.30729
8 | 2.0
9 | {39151939-5820-4E36-8A14-01454735BF8B}
10 | Exe
11 | Properties
12 | ScratchLog
13 | scratchlog
14 | v4.7
15 | 512
16 |
17 |
18 |
19 |
20 | 3.5
21 |
22 |
23 |
24 |
25 |
26 | true
27 | full
28 | false
29 | bin\Debug\
30 | DEBUG;TRACE
31 | prompt
32 | 4
33 | false
34 |
35 |
36 | pdbonly
37 | true
38 | bin\Release\
39 | TRACE
40 | prompt
41 | 4
42 | false
43 |
44 |
45 |
46 | ..\packages\Humanizer.Core.2.2.0\lib\netstandard1.0\Humanizer.dll
47 |
48 |
49 | ..\packages\Microsoft.Bcl.AsyncInterfaces.5.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll
50 |
51 |
52 | ..\packages\Microsoft.CodeAnalysis.Common.3.10.0\lib\netstandard2.0\Microsoft.CodeAnalysis.dll
53 |
54 |
55 | ..\packages\Microsoft.CodeAnalysis.CSharp.3.10.0\lib\netstandard2.0\Microsoft.CodeAnalysis.CSharp.dll
56 |
57 |
58 | ..\packages\Microsoft.CodeAnalysis.CSharp.Workspaces.3.10.0\lib\netstandard2.0\Microsoft.CodeAnalysis.CSharp.Workspaces.dll
59 |
60 |
61 | ..\packages\Microsoft.CodeAnalysis.Workspaces.Common.3.10.0\lib\netstandard2.0\Microsoft.CodeAnalysis.Workspaces.dll
62 |
63 |
64 |
65 | ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll
66 |
67 |
68 | ..\packages\System.Collections.Immutable.5.0.0\lib\net461\System.Collections.Immutable.dll
69 |
70 |
71 | ..\packages\System.Composition.AttributedModel.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.AttributedModel.dll
72 |
73 |
74 | ..\packages\System.Composition.Convention.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Convention.dll
75 |
76 |
77 | ..\packages\System.Composition.Hosting.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Hosting.dll
78 |
79 |
80 | ..\packages\System.Composition.Runtime.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.Runtime.dll
81 |
82 |
83 | ..\packages\System.Composition.TypedParts.1.0.31\lib\portable-net45+win8+wp8+wpa81\System.Composition.TypedParts.dll
84 |
85 |
86 | 3.5
87 |
88 |
89 | ..\packages\System.IO.Pipelines.5.0.1\lib\net461\System.IO.Pipelines.dll
90 |
91 |
92 | ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll
93 |
94 |
95 |
96 | ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll
97 |
98 |
99 | ..\packages\System.Reflection.Metadata.5.0.0\lib\net461\System.Reflection.Metadata.dll
100 |
101 |
102 | ..\packages\System.Runtime.CompilerServices.Unsafe.5.0.0\lib\net45\System.Runtime.CompilerServices.Unsafe.dll
103 |
104 |
105 | ..\packages\System.Text.Encoding.CodePages.4.5.1\lib\net461\System.Text.Encoding.CodePages.dll
106 |
107 |
108 | ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll
109 |
110 |
111 | 3.5
112 |
113 |
114 | 3.5
115 |
116 |
117 |
118 |
119 |
120 |
121 | Scratch.cs
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
138 |
139 |
140 |
141 |
142 |
143 |
150 |
--------------------------------------------------------------------------------
/gtk-ui/BookView.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using Gtk;
6 | using Barrkel.ScratchPad;
7 | using System.IO;
8 |
9 | namespace Barrkel.GtkScratchPad
10 | {
11 | public static class StringHelper
12 | {
13 | public static string EscapeMarkup(this string text)
14 | {
15 | return (text ?? "").Replace("&", "&").Replace("<", "<");
16 | }
17 | }
18 |
19 | public static class GdkHelper
20 | {
21 | private static Dictionary _keyNameMap = BuildKeyNameMap();
22 |
23 | public static bool TryGetKeyName(Gdk.Key key, out string name)
24 | {
25 | return _keyNameMap.TryGetValue(key, out name);
26 | }
27 |
28 | private static Dictionary BuildKeyNameMap()
29 | {
30 | Dictionary result = new Dictionary();
31 | Dictionary enumMap = new Dictionary();
32 |
33 | void tryAddNamed(string enumName, string name)
34 | {
35 | if (enumMap.TryGetValue(enumName, out Gdk.Key key))
36 | result.Add(key, name);
37 | }
38 |
39 | void tryAdd(string name)
40 | {
41 | tryAddNamed(name, name);
42 | }
43 |
44 | foreach (Gdk.Key key in Enum.GetValues(typeof(Gdk.Key)))
45 | {
46 | string enumName = Enum.GetName(typeof(Gdk.Key), key);
47 | enumMap[enumName] = key;
48 | }
49 | for (int i = 1; i <= 12; ++i)
50 | {
51 | string name = string.Format("F{0}", i);
52 | tryAdd(name);
53 | }
54 | for (char ch = 'A'; ch <= 'Z'; ++ch)
55 | {
56 | string name = ch.ToString();
57 | // Copy emacs logic for shift on normal characters.
58 | // map A to A
59 | tryAdd(name);
60 | // and a to a
61 | tryAdd(name.ToLower());
62 | }
63 | for (char ch = '0'; ch <= '9'; ++ch)
64 | {
65 | string name = ch.ToString();
66 | tryAddNamed("Key_" + name, name);
67 | }
68 | result.Add(Gdk.Key.quoteleft, "`");
69 | result.Add(Gdk.Key.quoteright, "'");
70 | result.Add(Gdk.Key.quotedbl, "\"");
71 | result.Add(Gdk.Key.exclam, "!");
72 | result.Add(Gdk.Key.at, "@");
73 | result.Add(Gdk.Key.numbersign, "#");
74 | result.Add(Gdk.Key.dollar, "$");
75 | result.Add(Gdk.Key.percent, "%");
76 | result.Add(Gdk.Key.asciicircum, "^");
77 | result.Add(Gdk.Key.ampersand, "&");
78 | result.Add(Gdk.Key.asterisk, "*");
79 | result.Add(Gdk.Key.parenleft, "(");
80 | result.Add(Gdk.Key.parenright, ")");
81 | result.Add(Gdk.Key.bracketleft, "[");
82 | result.Add(Gdk.Key.bracketright, "]");
83 | result.Add(Gdk.Key.braceleft, "{");
84 | result.Add(Gdk.Key.braceright, "}");
85 | result.Add(Gdk.Key.plus, "+");
86 | result.Add(Gdk.Key.minus, "-");
87 | result.Add(Gdk.Key.underscore, "_");
88 | result.Add(Gdk.Key.equal, "=");
89 | result.Add(Gdk.Key.slash, "/");
90 | result.Add(Gdk.Key.backslash, "\\");
91 | result.Add(Gdk.Key.bar, "|");
92 | result.Add(Gdk.Key.period, ".");
93 | result.Add(Gdk.Key.comma, ",");
94 | result.Add(Gdk.Key.less, "<");
95 | result.Add(Gdk.Key.greater, ">");
96 | result.Add(Gdk.Key.colon, ":");
97 | result.Add(Gdk.Key.semicolon, ";");
98 | result.Add(Gdk.Key.question, "?");
99 | result.Add(Gdk.Key.Escape, "Esc");
100 | result.Add(Gdk.Key.asciitilde, "~");
101 | result.Add(Gdk.Key.Page_Up, "PgUp");
102 | result.Add(Gdk.Key.Page_Down, "PgDn");
103 | result.Add(Gdk.Key.Home, "Home");
104 | result.Add(Gdk.Key.End, "End");
105 |
106 | result.Add(Gdk.Key.Up, "Up");
107 | result.Add(Gdk.Key.Down, "Down");
108 | result.Add(Gdk.Key.Left, "Left");
109 | result.Add(Gdk.Key.Right, "Right");
110 |
111 | result.Add(Gdk.Key.Return, "Return");
112 | result.Add(Gdk.Key.Delete, "Delete");
113 | result.Add(Gdk.Key.Insert, "Insert");
114 | result.Add(Gdk.Key.BackSpace, "BackSpace");
115 | result.Add(Gdk.Key.space, "Space");
116 | result.Add(Gdk.Key.Tab, "Tab");
117 | // this comes out with S-Tab
118 | result.Add(Gdk.Key.ISO_Left_Tab, "Tab");
119 |
120 | // Clobber synonym. L1 and F11 have same value.
121 | result[Gdk.Key.L1] = "F11";
122 | result[Gdk.Key.L2] = "F12";
123 |
124 | return result;
125 | }
126 | }
127 |
128 | public delegate void KeyEventHandler(Gdk.EventKey evnt, ref bool handled);
129 |
130 | public class MyTextView : TextView
131 | {
132 | public event KeyEventHandler KeyDownEvent;
133 |
134 | [GLib.DefaultSignalHandler(Type = typeof(Widget), ConnectionMethod = "OverrideKeyPressEvent")]
135 | protected override bool OnKeyPressEvent(Gdk.EventKey evnt)
136 | {
137 | bool handled = false;
138 | KeyDownEvent?.Invoke(evnt, ref handled);
139 | if (handled)
140 | return true;
141 | else
142 | return base.OnKeyPressEvent(evnt);
143 | }
144 | }
145 |
146 | public class BookView : Frame, IScratchBookView
147 | {
148 | private static readonly Gdk.Atom GlobalClipboard = Gdk.Atom.Intern("CLIPBOARD", false);
149 |
150 | DateTime _lastModification;
151 | DateTime _lastSave;
152 | bool _dirty;
153 | int _currentPage;
154 | // If non-null, then browsing history.
155 | ScratchIterator _currentIterator;
156 | MyTextView _textView;
157 | bool _settingText;
158 | Label _titleLabel;
159 | Label _dateLabel;
160 | Label _pageLabel;
161 | Label _versionLabel;
162 | List _deferred = new List();
163 |
164 | public BookView(ScratchBook book, ScratchBookController controller, Window appWindow)
165 | {
166 | AppWindow = appWindow;
167 | Book = book;
168 | Controller = controller;
169 | AppSettings = controller.Scope;
170 | InitComponent();
171 | _currentPage = book.Pages.Count > 0 ? book.Pages.Count - 1 : 0;
172 | UpdateViewLabels();
173 | UpdateTextBox();
174 |
175 | Controller.ConnectView(this);
176 | }
177 |
178 | public ScratchBookController Controller { get; }
179 | public Window AppWindow { get; private set; }
180 | public ScratchScope AppSettings { get; }
181 |
182 | private void UpdateTextBox()
183 | {
184 | _settingText = true;
185 | try
186 | {
187 | if (_currentPage >= Book.Pages.Count)
188 | _textView.Buffer.Text = "";
189 | else if (_currentIterator != null)
190 | _textView.Buffer.Text = _currentIterator.Text;
191 | else
192 | _textView.Buffer.Text = Book.Pages[_currentPage].Text;
193 | TextIter iter = _textView.Buffer.GetIterAtOffset(0);
194 | _textView.Buffer.SelectRange(iter, iter);
195 | _textView.ScrollToIter(iter, 0, false, 0, 0);
196 | }
197 | finally
198 | {
199 | _settingText = false;
200 | }
201 | }
202 |
203 | private void UpdateTitle()
204 | {
205 | _titleLabel.Markup = GetTitleMarkup(new StringReader(_textView.Buffer.Text).ReadLine());
206 | }
207 |
208 | private void UpdateViewLabels()
209 | {
210 | if (_currentPage >= Book.Pages.Count)
211 | {
212 | _dateLabel.Text = "";
213 | _pageLabel.Text = "";
214 | _versionLabel.Text = "";
215 | }
216 | else
217 | {
218 | _pageLabel.Markup = GetPageMarkup(_currentPage + 1, Book.Pages.Count);
219 | if (_currentIterator == null)
220 | {
221 | _versionLabel.Markup = GetInfoMarkup("Latest");
222 | _dateLabel.Markup = GetInfoMarkup(
223 | Book.Pages[_currentPage].ChangeStamp.ToLocalTime().ToString("F"));
224 | }
225 | else
226 | {
227 | _versionLabel.Markup = GetInfoMarkup(string.Format("Version {0} of {1}",
228 | _currentIterator.Position, _currentIterator.Count));
229 | _dateLabel.Markup = GetInfoMarkup(_currentIterator.Stamp.ToLocalTime().ToString("F"));
230 | }
231 | }
232 | }
233 |
234 | public void AddNewPage()
235 | {
236 | EnsureSaved();
237 | _currentPage = Book.Pages.Count;
238 | UpdateTextBox();
239 | UpdateViewLabels();
240 | _dirty = false;
241 | }
242 |
243 | public void EnsureSaved()
244 | {
245 | Book.EnsureSaved();
246 | if (!_dirty)
247 | return;
248 | string currentText = _textView.Buffer.Text;
249 | if (_currentPage >= Book.Pages.Count)
250 | {
251 | if (currentText == "")
252 | return;
253 | Book.AddPage();
254 | UpdateViewLabels();
255 | }
256 | Book.Pages[_currentPage].Text = _textView.Buffer.Text;
257 | Book.SaveLatest();
258 | _lastSave = DateTime.UtcNow;
259 | _dirty = false;
260 | _currentPage = Book.MoveToEnd(_currentPage);
261 | UpdateViewLabels();
262 | }
263 |
264 | private static string GetTitleMarkup(string title)
265 | {
266 | return string.Format("{0}", title.EscapeMarkup());
267 | }
268 |
269 | private static string GetPageMarkup(int page, int total)
270 | {
271 | return string.Format("Page {0} of {1}",
272 | page, total);
273 | }
274 |
275 | private static string GetInfoMarkup(string info)
276 | {
277 | return string.Format("{0}", info.EscapeMarkup());
278 | }
279 |
280 | private static readonly Gdk.Color LightBlue = new Gdk.Color(207, 207, 239);
281 |
282 | private void InitComponent()
283 | {
284 | Gdk.Color grey = new Gdk.Color(0xA0, 0xA0, 0xA0);
285 | Gdk.Color noteBackground = LightBlue;
286 | if (!Gdk.Color.Parse(AppSettings.GetOrDefault("background-color", "#cfcfef"), ref noteBackground))
287 | noteBackground = LightBlue;
288 |
289 | var infoFont = Pango.FontDescription.FromString(Controller.Scope.GetOrDefault("info-font", "Verdana"));
290 | var textFont = Pango.FontDescription.FromString(Controller.Scope.GetOrDefault("text-font", "Courier New"));
291 |
292 | _textView = new MyTextView
293 | {
294 | WrapMode = WrapMode.Word
295 | };
296 | _textView.ModifyBase(StateType.Normal, noteBackground);
297 | _textView.Buffer.Changed += _text_TextChanged;
298 | _textView.KeyDownEvent += _textView_KeyDownEvent;
299 | _textView.ModifyFont(textFont);
300 |
301 | ScrolledWindow scrolledTextView = new ScrolledWindow();
302 | scrolledTextView.Add(_textView);
303 |
304 | _titleLabel = new Label();
305 | _titleLabel.SetAlignment(0, 0);
306 | _titleLabel.Justify = Justification.Left;
307 | _titleLabel.SetPadding(0, 5);
308 | _titleLabel.ModifyFont(textFont);
309 | GLib.Timeout.Add((uint) TimeSpan.FromSeconds(3).TotalMilliseconds,
310 | () => { return SaveTimerTick(); });
311 |
312 | EventBox titleContainer = new EventBox();
313 | titleContainer.Add(_titleLabel);
314 |
315 | // hbox
316 | // vbox
317 | // dateLabel
318 | // versionLabel
319 | // pageLabel
320 | HBox locationInfo = new HBox();
321 | VBox locationLeft = new VBox();
322 |
323 | _dateLabel = new Label
324 | {
325 | Justify = Justification.Left
326 | };
327 | _dateLabel.SetAlignment(0, 0);
328 | _dateLabel.SetPadding(5, 5);
329 | _dateLabel.ModifyFont(infoFont);
330 | locationLeft.PackStart(_dateLabel, false, false, 0);
331 |
332 | _versionLabel = new Label();
333 | _versionLabel.SetAlignment(0, 0);
334 | _versionLabel.Justify = Justification.Left;
335 | _versionLabel.SetPadding(5, 5);
336 | _versionLabel.ModifyFont(infoFont);
337 | locationLeft.PackStart(_versionLabel, false, false, 0);
338 |
339 | locationInfo.PackStart(locationLeft, true, true, 0);
340 |
341 | _pageLabel = new Label
342 | {
343 | Markup = GetPageMarkup(1, 5),
344 | Justify = Justification.Right
345 | };
346 | _pageLabel.SetAlignment(1, 0.5f);
347 | _pageLabel.SetPadding(5, 5);
348 | _pageLabel.ModifyFont(infoFont);
349 | locationInfo.PackEnd(_pageLabel, true, true, 0);
350 | Pango.Context ctx = _pageLabel.PangoContext;
351 |
352 | VBox outerVertical = new VBox();
353 | outerVertical.PackStart(titleContainer, false, false, 0);
354 | outerVertical.PackStart(scrolledTextView, true, true, 0);
355 | outerVertical.PackEnd(locationInfo, false, false, 0);
356 |
357 | Add(outerVertical);
358 |
359 | BorderWidth = 5;
360 |
361 | }
362 |
363 | void Defer(System.Action action)
364 | {
365 | _deferred.Add(action);
366 | GLib.Idle.Add(DrainDeferred);
367 | }
368 |
369 | bool DrainDeferred()
370 | {
371 | if (_deferred.Count == 0)
372 | return false;
373 | System.Action defer = _deferred[_deferred.Count - 1];
374 | _deferred.RemoveAt(_deferred.Count - 1);
375 | defer();
376 |
377 | return true;
378 | }
379 |
380 | void _textView_KeyDownEvent(Gdk.EventKey evnt, ref bool handled)
381 | {
382 | // FIXME: this event actually doesn't grab that much.
383 | // E.g. normal Up, Down, Ctrl-Left, Ctrl-Right etc. are not seen here
384 | // It doesn't see typed characters. Effectively it only sees F-keys and keys pressed with Alt.
385 | bool ctrl = (evnt.State & Gdk.ModifierType.ControlMask) != 0;
386 | bool alt = (evnt.State & Gdk.ModifierType.Mod1Mask) != 0;
387 | bool shift = (evnt.State & Gdk.ModifierType.ShiftMask) != 0;
388 |
389 | if (GdkHelper.TryGetKeyName(evnt.Key, out string keyName))
390 | {
391 | handled = Controller.InformKeyStroke(this, keyName, ctrl, alt, shift);
392 | }
393 | else
394 | {
395 | if (Controller.Scope.GetOrDefault("debug-keys", false))
396 | Log.Out($"Not mapped: {evnt.Key}");
397 | }
398 |
399 | var state = evnt.State & Gdk.ModifierType.Mod1Mask;
400 | switch (state)
401 | {
402 | case Gdk.ModifierType.Mod1Mask:
403 | switch (evnt.Key)
404 | {
405 | case Gdk.Key.Home: // M-Home
406 | case Gdk.Key.Up: // M-Up
407 | PreviousVersion();
408 | break;
409 |
410 | case Gdk.Key.End: // M-End
411 | case Gdk.Key.Down: // M-Down
412 | NextVersion();
413 | break;
414 |
415 | default:
416 | return;
417 | }
418 | break;
419 | }
420 | }
421 |
422 | private void _text_TextChanged(object sender, EventArgs e)
423 | {
424 | UpdateTitle();
425 | if (_settingText)
426 | return;
427 |
428 | if (!_dirty)
429 | _lastSave = DateTime.UtcNow;
430 | _lastModification = DateTime.UtcNow;
431 | _currentIterator = null;
432 | _dirty = true;
433 | }
434 |
435 | private bool SaveTimerTick()
436 | {
437 | if (!_dirty)
438 | return true;
439 |
440 | TimeSpan span = DateTime.UtcNow - _lastModification;
441 | if (span > TimeSpan.FromSeconds(5))
442 | {
443 | EnsureSaved();
444 | return true;
445 | }
446 |
447 | span = DateTime.UtcNow - _lastSave;
448 | if (span > TimeSpan.FromSeconds(20))
449 | EnsureSaved();
450 |
451 | return true;
452 | }
453 |
454 | public bool Navigate(Func callback)
455 | {
456 | EnsureSaved();
457 | if (_currentPage >= Book.Pages.Count)
458 | return false;
459 | if (_currentIterator == null)
460 | {
461 | _currentIterator = Book.Pages[_currentPage].GetIterator();
462 | _currentIterator.MoveToEnd();
463 | }
464 | if (callback(_currentIterator))
465 | {
466 | UpdateTextBox();
467 | SetSelection(_currentIterator.UpdatedFrom, _currentIterator.UpdatedTo);
468 | ScrollIntoView(_currentIterator.UpdatedFrom);
469 | UpdateViewLabels();
470 | return true;
471 | }
472 | return false;
473 | }
474 |
475 | void PreviousVersion()
476 | {
477 | Navigate(iter => iter.MovePrevious());
478 | }
479 |
480 | void NextVersion()
481 | {
482 | Navigate(iter => iter.MoveNext());
483 | }
484 |
485 | public void JumpToPage(int pageIndex)
486 | {
487 | if (pageIndex < 0 || pageIndex >= Book.Pages.Count || pageIndex == _currentPage)
488 | return;
489 |
490 | ExitPage();
491 | _currentIterator = null;
492 | _currentPage = Book.MoveToEnd(pageIndex);
493 | EnterPage();
494 | }
495 |
496 | private void ExitPage()
497 | {
498 | EnsureSaved();
499 | Controller.InvokeAction(this, "exit-page", ScratchValue.EmptyList);
500 | }
501 |
502 | private void EnterPage()
503 | {
504 | UpdateTextBox();
505 | UpdateTitle();
506 | UpdateViewLabels();
507 | Controller.InvokeAction(this, "enter-page", ScratchValue.EmptyList);
508 | }
509 |
510 | public void InsertText(string text)
511 | {
512 | _textView.Buffer.InsertAtCursor(text);
513 | }
514 |
515 | public void ScrollIntoView(int pos)
516 | {
517 | Defer(() =>
518 | {
519 | _textView.ScrollToIter(_textView.Buffer.GetIterAtOffset(pos), 0, false, 0, 0);
520 | });
521 | }
522 |
523 | public int ScrollPos
524 | {
525 | get
526 | {
527 | // Returns the position in the text of location 0,0 at top left of the view
528 | _textView.WindowToBufferCoords(TextWindowType.Text, 0, 0, out int x, out int y);
529 | TextIter iter = _textView.GetIterAtLocation(x, y);
530 | return iter.Offset;
531 | }
532 | set
533 | {
534 | Defer(() =>
535 | {
536 | _textView.ScrollToIter(_textView.Buffer.GetIterAtOffset(int.MaxValue), 0, false, 0, 0);
537 | _textView.ScrollToIter(_textView.Buffer.GetIterAtOffset(value), 0, false, 0, 0);
538 | });
539 | }
540 | }
541 |
542 | public void SetSelection(int from, int to)
543 | {
544 | // hack: just highlight first few characters
545 | to = from + 4;
546 | if (from > to)
547 | {
548 | int t = from;
549 | from = to;
550 | to = t;
551 | }
552 | if (from == to)
553 | {
554 | to = from + 1;
555 | }
556 | _textView.Buffer.MoveMark("insert", _textView.Buffer.GetIterAtOffset(from));
557 | _textView.Buffer.MoveMark("selection_bound", _textView.Buffer.GetIterAtOffset(to));
558 | }
559 |
560 | public void AddRepeatingTimer(int millis, string actionName)
561 | {
562 | GLib.Timeout.Add((uint) millis, () =>
563 | {
564 | Controller.InvokeAction(this, actionName, ScratchValue.EmptyList);
565 | return true;
566 | });
567 | }
568 |
569 | public int CurrentPosition
570 | {
571 | get => _textView.Buffer.CursorPosition;
572 | set => _textView.Buffer.MoveMark("insert", _textView.Buffer.GetIterAtOffset(value));
573 | }
574 |
575 | public int CurrentPageIndex
576 | {
577 | get => _currentPage;
578 | set
579 | {
580 | if (value == _currentPage || value >= Book.Pages.Count || value < 0)
581 | return;
582 | ExitPage();
583 | _currentPage = value;
584 | EnterPage();
585 | }
586 | }
587 |
588 | public ScratchBook Book
589 | {
590 | get; private set;
591 | }
592 |
593 | (int, int) IScratchBookView.Selection
594 | {
595 | get
596 | {
597 | _textView.Buffer.GetSelectionBounds(out var start, out var end);
598 | return (start.Offset, end.Offset);
599 | }
600 | set
601 | {
602 | var (from, to) = value;
603 | _textView.Buffer.MoveMark("insert", _textView.Buffer.GetIterAtOffset(from));
604 | _textView.Buffer.MoveMark("selection_bound", _textView.Buffer.GetIterAtOffset(to));
605 | }
606 | }
607 |
608 | string IScratchBookView.CurrentText
609 | {
610 | get => _textView.Buffer.Text;
611 | }
612 |
613 | string IScratchBookView.Clipboard
614 | {
615 | get
616 | {
617 | using (Clipboard clip = Clipboard.Get(GlobalClipboard))
618 | return clip.WaitForText();
619 | }
620 | }
621 |
622 | void IScratchBookView.DeleteTextBackwards(int count)
623 | {
624 | int pos = CurrentPosition;
625 | TextIter end = _textView.Buffer.GetIterAtOffset(pos);
626 | TextIter start = _textView.Buffer.GetIterAtOffset(pos - count);
627 | _textView.Buffer.Delete(ref start, ref end);
628 | }
629 |
630 | public bool RunSearch(ScratchPad.SearchFunc searchFunc, out T result)
631 | {
632 | return SearchWindow.RunSearch(AppWindow, searchFunc.Invoke, AppSettings, out result);
633 | }
634 |
635 | public bool GetInput(ScratchScope settings, out string value)
636 | {
637 | return InputModalWindow.GetInput(AppWindow, settings, out value);
638 | }
639 |
640 | string IScratchBookView.SelectedText
641 | {
642 | get
643 | {
644 | _textView.Buffer.GetSelectionBounds(out TextIter start, out TextIter end);
645 | return _textView.Buffer.GetText(start, end, true);
646 | }
647 | set
648 | {
649 | _textView.Buffer.GetSelectionBounds(out TextIter start, out TextIter end);
650 | _textView.Buffer.Delete(ref start, ref end);
651 | if (!string.IsNullOrEmpty(value))
652 | _textView.Buffer.Insert(ref end, value);
653 | start = _textView.Buffer.GetIterAtOffset(end.Offset - value.Length);
654 | _textView.Buffer.SelectRange(start, end);
655 | }
656 | }
657 |
658 | public void LaunchSnippet(ScratchScope settings)
659 | {
660 | SnippetWindow window = new SnippetWindow(settings);
661 | window.ShowAll();
662 | }
663 | }
664 | }
665 |
--------------------------------------------------------------------------------
/gtk-ui/ScratchLegacy.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Diagnostics;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Text.RegularExpressions;
7 |
8 | namespace Barrkel.ScratchPad
9 | {
10 | public class LegacyLibrary : NativeLibrary
11 | {
12 | public static readonly LegacyLibrary Instance = new LegacyLibrary();
13 |
14 | static readonly char[] SmartChars = { '{', '[', '(' };
15 | static readonly char[] SmartInversion = { '}', ']', ')' };
16 |
17 | private LegacyLibrary() : base("legacy")
18 | {
19 | // TODO: move these to config somehow
20 | // E.g. .globalconfig which can be exported from resource
21 | // "Invoking" a string looks up the binding and invokes that, recursively.
22 | // Keys are bound by binding their names.
23 | // Keys may be bound to an action / scratch function directly,
24 | // but because of the string invocation action, indirection works too.
25 | Bind("F4", new ScratchValue("insert-date"));
26 | Bind("S-F4", new ScratchValue("insert-datetime"));
27 | Bind("Return", new ScratchValue("autoindent-return"));
28 | Bind("Tab", new ScratchValue("indent-block"));
29 | Bind("S-Tab", new ScratchValue("unindent-block"));
30 | Bind("C-v", new ScratchValue("smart-paste"));
31 | Bind("M-/", new ScratchValue("complete"));
32 | Bind("C-a", new ScratchValue("goto-sol"));
33 | Bind("C-e", new ScratchValue("goto-eol"));
34 | Bind("F12", new ScratchValue("navigate-title"));
35 | Bind("F11", new ScratchValue("navigate-contents"));
36 | Bind("C-t", new ScratchValue("navigate-todo"));
37 | Bind("C-n", new ScratchValue("add-new-page"));
38 |
39 | Bind("F5", new ScratchValue("load-config"));
40 | Bind("S-F5", new ScratchValue("dump-bindings"));
41 | }
42 |
43 | [TypedAction("dump-bindings")]
44 | public void DumpBindings(ExecutionContext context, IList args)
45 | {
46 | var rootController = context.Controller.RootController;
47 | Log.Out("For root:");
48 | foreach (var entry in rootController.RootScope)
49 | Log.Out($" {entry.Item1} => {entry.Item2}");
50 | foreach (var book in rootController.Root.Books)
51 | {
52 | Log.Out($"For book: {book.Name}");
53 | foreach (var entry in rootController.GetControllerFor(book).Scope)
54 | Log.Out($" {entry.Item1} => {entry.Item2}");
55 | }
56 | }
57 |
58 | [TypedAction("get-cursor-text-re", ScratchType.String)]
59 | public ScratchValue DoGetCursorTextRe(ExecutionContext context, IList args)
60 | {
61 | // get-cursor-text-regex(regex) -> returns earliest text matching regex under cursor
62 | // Because regex won't scan backwards and we want to extract a word under cursor,
63 | // we use a hokey algorithm:
64 | // start := cursorPos
65 | // result := ''
66 | // begin loop
67 | // match regex at start
68 | // if match does not include cursor position, break
69 | // result := regex match
70 | // set start to start - 1
71 | // end loop
72 | // return result
73 | int currPos = context.View.CurrentPosition;
74 | // Regex caches compiled regexes, we expect reuse of the same extractions.
75 | Regex re = new Regex(args[0].StringValue,
76 | RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled,
77 | TimeSpan.FromSeconds(1));
78 | string match = "";
79 | string currentText = context.View.CurrentText;
80 | int startPos = currPos;
81 | while (startPos > 0)
82 | {
83 | Match m = re.Match(currentText, startPos);
84 | if (!m.Success)
85 | // but we allow one step back for cursor at end
86 | if (startPos == currPos)
87 | {
88 | --startPos;
89 | continue;
90 | }
91 | else
92 | break;
93 |
94 | // regex must match immediately
95 | if (m.Index != startPos)
96 | {
97 | // second chance, step back
98 | if (startPos == currPos)
99 | {
100 | --startPos;
101 | continue;
102 | }
103 | break;
104 | }
105 | // match must include cursor (cursor at end is ok though)
106 | if (m.Index + m.Length < currPos - 1)
107 | break;
108 | match = m.Value;
109 | --startPos;
110 | }
111 | return match == ""
112 | ? ScratchValue.Null
113 | : new ScratchValue(match);
114 | }
115 |
116 | [VariadicAction("dp")]
117 | public void DoDebugPrint(ExecutionContext context, IList args)
118 | {
119 | Log.Out(string.Join(" ", args));
120 | }
121 |
122 | [VariadicAction("log")]
123 | public void DoLog(ExecutionContext context, IList args)
124 | {
125 | Log.Out(string.Join("", args.Select(x => "" + x.ObjectValue)));
126 | }
127 |
128 | [TypedAction("insert-date")]
129 | public void DoInsertDate(ExecutionContext context, IList args)
130 | {
131 | context.View.InsertText(DateTime.Today.ToString("yyyy-MM-dd"));
132 | }
133 |
134 | [TypedAction("insert-datetime")]
135 | public void DoInsertDateTime(ExecutionContext context, IList args)
136 | {
137 | context.View.InsertText(DateTime.Now.ToString("yyyy-MM-dd HH:mm"));
138 | }
139 |
140 | [TypedAction("autoindent-return")]
141 | public void DoAutoindentReturn(ExecutionContext context, IList args)
142 | {
143 | string text = context.View.CurrentText;
144 | int pos = context.View.CurrentPosition;
145 | string indent = GetCurrentIndent(text, pos);
146 |
147 | switch (IsSmartDelimiter(text, pos - 1, out char closer))
148 | {
149 | case SmartDelimiter.No:
150 | context.View.InsertText(string.Format("\n{0}", indent));
151 | break;
152 |
153 | case SmartDelimiter.Yes:
154 | // smart { etc.
155 | context.View.InsertText(string.Format("\n{0} \n{0}{1}", indent, closer));
156 | context.View.CurrentPosition -= (1 + indent.Length + 1);
157 | context.View.Selection = (context.View.CurrentPosition, context.View.CurrentPosition);
158 | break;
159 |
160 | case SmartDelimiter.IndentOnly:
161 | context.View.InsertText(string.Format("\n{0} ", indent));
162 | break;
163 | }
164 | context.View.ScrollIntoView(context.View.CurrentPosition);
165 | }
166 |
167 | [TypedAction("smart-paste")]
168 | public void DoSmartPaste(ExecutionContext context, IList args)
169 | {
170 | context.View.SelectedText = "";
171 | string textToPaste = context.View.Clipboard;
172 | if (string.IsNullOrEmpty(textToPaste))
173 | return;
174 | if (context.Scope.TryLookup("paste-filter", out var pasteFilter))
175 | textToPaste = pasteFilter.Invoke("paste-filter", context, new ScratchValue(textToPaste)).StringValue;
176 | string indent = GetCurrentIndent(context.View.CurrentText, context.View.CurrentPosition);
177 | if (indent.Length > 0)
178 | // Remove existing indent if pasted to an indent
179 | context.View.InsertText(AddIndent(indent, ResetIndent(textToPaste), IndentOptions.SkipFirst));
180 | else
181 | // Preserve existing indent if from col 0
182 | context.View.InsertText(AddIndent(indent, textToPaste, IndentOptions.SkipFirst));
183 | context.View.ScrollIntoView(context.View.CurrentPosition);
184 | }
185 |
186 | private int GetIndent(string text)
187 | {
188 | int result = 0;
189 | foreach (char ch in text)
190 | {
191 | if (!char.IsWhiteSpace(ch))
192 | return result;
193 | if (ch == '\t')
194 | result += (8 - result % 8);
195 | else
196 | ++result;
197 | }
198 | return result;
199 | }
200 |
201 | private const int DefaultContextLength = 40;
202 |
203 | // Given a line and a match, add prefix and postfix text as necessary, and mark up match with [].
204 | // This logic is sufficiently UI-specific that it should be factored out somehow.
205 | static string Contextualize(string line, SimpleMatch match, int contextLength = DefaultContextLength)
206 | {
207 | StringBuilder result = new StringBuilder();
208 | // ... preamble [match] postamble ...
209 | if (match.Start > contextLength)
210 | {
211 | result.Append("...");
212 | result.Append(line.Substring(match.Start - contextLength + 3, contextLength - 3));
213 | }
214 | else
215 | {
216 | result.Append(line.Substring(0, match.Start));
217 | }
218 | result.Append('[').Append(match.Value).Append(']');
219 | if (match.End + contextLength < line.Length)
220 | {
221 | result.Append(line.Substring(match.End, contextLength - 3));
222 | result.Append("...");
223 | }
224 | else
225 | {
226 | result.Append(line.Substring(match.End));
227 | }
228 | return result.ToString();
229 | }
230 |
231 | struct SimpleMatch
232 | {
233 | public SimpleMatch(string text, Match match)
234 | {
235 | Text = text;
236 | if (!match.Success)
237 | {
238 | Start = 0;
239 | End = -1;
240 | }
241 | else
242 | {
243 | Start = match.Index;
244 | End = match.Index + match.Value.Length;
245 | }
246 | }
247 |
248 | private SimpleMatch(string text, int start, int end)
249 | {
250 | Start = start;
251 | End = end;
252 | Text = text;
253 | }
254 |
255 | public SimpleMatch Extend(SimpleMatch other)
256 | {
257 | if (!object.ReferenceEquals(Text, other.Text))
258 | throw new ArgumentException("Extend may only be called with match over same text");
259 | return new SimpleMatch(Text, Math.Min(Start, other.Start), Math.Max(End, other.End));
260 | }
261 |
262 | public int Start { get; }
263 | public int End { get; }
264 | public int Length => End - Start;
265 | private string Text { get; }
266 | public string Value => Text.Substring(Start, Length);
267 | // We're not interested in 0-length matches
268 | public bool Success => Length > 0;
269 | }
270 |
271 | private static List MergeMatches(List matches)
272 | {
273 | matches.Sort((a, b) => a.Start.CompareTo(b.Start));
274 | List result = new List();
275 | foreach (SimpleMatch m in matches)
276 | {
277 | if (result.Count == 0 || result[result.Count - 1].End < m.Start)
278 | result.Add(m);
279 | else
280 | result[result.Count - 1] = result[result.Count - 1].Extend(m);
281 | }
282 | return result;
283 | }
284 |
285 | static List MatchRegexList(List regexes, string text)
286 | {
287 | var result = new List();
288 | foreach (Regex regex in regexes)
289 | {
290 | var matches = regex.Matches(text);
291 | if (matches.Count == 0)
292 | return new List();
293 | result.AddRange(matches.Cast().Select(x => new SimpleMatch(text, x)));
294 | }
295 | return MergeMatches(result);
296 | }
297 |
298 | // Given a list of (lineOffset, line) and a pattern, return UI-suitable strings with associated (offset, length) pairs.
299 | static IEnumerable<(string, (int, int))> FindMatchingLocations(List<(int, string)> lines, List regexes)
300 | {
301 | int count = 0;
302 | int linum = 0;
303 | // We cap at 1000 just in case caller doesn't limit us
304 | while (count < 1000 && linum < lines.Count)
305 | {
306 | var (lineOfs, line) = lines[linum];
307 | ++linum;
308 |
309 | // Default case: empty pattern. Special case this one, we don't need to split every character.
310 | if (regexes.Count == 0 || regexes[0].IsMatch(""))
311 | {
312 | ++count;
313 | yield return (line, (lineOfs, 0));
314 | continue;
315 | }
316 |
317 | var matches = MatchRegexList(regexes, line);
318 | if (matches.Count == 0)
319 | {
320 | continue;
321 | }
322 | count += matches.Count;
323 | // if multiple matches in a line, break them out
324 | // if a single match, keep as is
325 | string prefix = "";
326 | if (matches.Count > 1)
327 | {
328 | prefix = " ";
329 | yield return (line, (lineOfs + matches[0].Start, matches[0].Length));
330 | }
331 | foreach (SimpleMatch match in matches)
332 | {
333 | yield return (prefix + Contextualize(line, match), (lineOfs + match.Start, match.Length));
334 | }
335 | }
336 | }
337 |
338 | // Returns (lineOffset, line) without trailing line separators.
339 | // lineOffset is the character offset of the start of the line in text.
340 | static IEnumerable<(int, string)> GetNonEmptyLines(string text)
341 | {
342 | int pos = 0;
343 | while (pos < text.Length)
344 | {
345 | char ch = text[pos];
346 | int start = pos;
347 | ++pos;
348 | if (ch == '\n' || ch == '\r' || pos == text.Length)
349 | continue;
350 |
351 | while (pos < text.Length)
352 | {
353 | ch = text[pos];
354 | ++pos;
355 | if (ch == '\n' || ch == '\r')
356 | break;
357 | }
358 | yield return (start, text.Substring(start, pos - start - 1));
359 | }
360 | }
361 |
362 | private bool AnyCaps(string value)
363 | {
364 | foreach (char ch in value)
365 | if (char.IsUpper(ch))
366 | return true;
367 | return false;
368 | }
369 |
370 | public List ParseRegexList(string pattern, RegexOptions options)
371 | {
372 | if (!AnyCaps(pattern))
373 | options |= RegexOptions.IgnoreCase;
374 |
375 | Regex parsePart(string part)
376 | {
377 | if (part.StartsWith("*"))
378 | part = "\\" + part;
379 | try
380 | {
381 | return new Regex(part, options);
382 | }
383 | catch (ArgumentException)
384 | {
385 | return new Regex(Regex.Escape(part), options);
386 | }
387 | }
388 |
389 | return pattern.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries)
390 | .Select(x => parsePart(x))
391 | .ToList();
392 | }
393 |
394 | [TypedAction("navigate-title")]
395 | public void NavigateTitle(ExecutionContext context, IList args)
396 | {
397 | context.View.EnsureSaved();
398 | if (context.View.RunSearch(text => context.View.Book.SearchTitles(text).Take(100), out int found))
399 | context.View.JumpToPage(found);
400 | }
401 |
402 | [TypedAction("navigate-contents")]
403 | public void NavigateContents(ExecutionContext context, IList args)
404 | {
405 | context.View.EnsureSaved();
406 | if (context.View.RunSearch(text => TrySearch(context.View.Book, text).Take(50), out var triple))
407 | {
408 | var (page, pos, len) = triple;
409 | context.View.JumpToPage(page);
410 | // these should probably be part of JumpToPage, to avoid the default action
411 | context.View.ScrollIntoView(pos);
412 | context.View.Selection = (pos, pos + len);
413 | }
414 | }
415 |
416 | [TypedAction("navigate-todo")]
417 | public void NavigateTodo(ExecutionContext context, IList args)
418 | {
419 | NavigateSigil(context, ScratchValue.List("=>"));
420 | }
421 |
422 | [TypedAction("add-new-page")]
423 | public void AddNewPage(ExecutionContext context, IList args)
424 | {
425 | context.View.AddNewPage();
426 | }
427 |
428 | [TypedAction("on-text-changed")]
429 | public void OnTextChanged(ExecutionContext context, IList args)
430 | {
431 | // text has changed
432 | }
433 |
434 | [TypedAction("indent-block")]
435 | public void DoIndentBlock(ExecutionContext context, IList args)
436 | {
437 | string text = context.View.SelectedText;
438 | if (string.IsNullOrEmpty(text))
439 | {
440 | context.View.InsertText(" ");
441 | return;
442 | }
443 | context.View.SelectedText = AddIndent(" ", text, IndentOptions.SkipTrailingEmpty);
444 | }
445 |
446 | [TypedAction("unindent-block")]
447 | public void DoUnindentBlock(ExecutionContext context, IList args)
448 | {
449 | // remove 2 spaces or a tab or one space from every line
450 | string text = context.View.SelectedText;
451 | if (string.IsNullOrEmpty(text))
452 | {
453 | // We insert a literal tab, but we could consider unindenting line.
454 | context.View.InsertText("\t");
455 | return;
456 | }
457 | string[] lines = text.Split('\r', '\n');
458 | for (int i = 0; i < lines.Length; ++i)
459 | {
460 | string line = lines[i];
461 | if (line.Length == 0)
462 | continue;
463 | if (line.StartsWith(" "))
464 | lines[i] = line.Substring(2);
465 | else if (line.StartsWith("\t"))
466 | lines[i] = line.Substring(1);
467 | else if (line.StartsWith(" "))
468 | lines[i] = line.Substring(1);
469 | }
470 | context.View.SelectedText = string.Join("\n", lines);
471 | }
472 |
473 | [TypedAction("exit-page")]
474 | public void ExitPage(ExecutionContext context, IList args)
475 | {
476 | ScratchPage page = GetPage(context.View.Book, context.View.CurrentPageIndex);
477 | if (page == null)
478 | return;
479 | PageViewState state = page.GetViewState(context.View, PageViewState.Create);
480 | state.CurrentSelection = context.View.Selection;
481 | state.CurrentScrollPos = context.View.ScrollPos;
482 | }
483 |
484 | [TypedAction("enter-page")]
485 | public void EnterPage(ExecutionContext context, IList args)
486 | {
487 | ScratchPage page = GetPage(context.View.Book, context.View.CurrentPageIndex);
488 | if (page == null)
489 | return;
490 | PageViewState state = page.GetViewState(context.View, PageViewState.Create);
491 | if (state.CurrentSelection.HasValue)
492 | context.View.Selection = state.CurrentSelection.Value;
493 | if (state.CurrentScrollPos.HasValue)
494 | context.View.ScrollPos = state.CurrentScrollPos.Value;
495 | }
496 |
497 | [Flags]
498 | enum SearchOptions
499 | {
500 | None = 0,
501 | TitleLinkToFirstResult = 1
502 | }
503 |
504 | // returns (UI line, (page, pos, len))
505 | private IEnumerable<(string, (int, int, int))> TrySearch(ScratchBook book, string pattern,
506 | SearchOptions options = SearchOptions.None)
507 | {
508 | List re, fullRe;
509 | try
510 | {
511 | re = ParseRegexList(pattern, RegexOptions.Singleline);
512 | fullRe = ParseRegexList(pattern, RegexOptions.Multiline);
513 | }
514 | catch (ArgumentException)
515 | {
516 | yield break;
517 | }
518 |
519 | // Keep most recent pagesfirst
520 | for (int i = book.Pages.Count - 1; i >= 0; --i)
521 | {
522 | var page = book.Pages[i];
523 | if (fullRe.Count > 0 && !fullRe[0].Match(page.Text).Success)
524 | continue;
525 |
526 | if (pattern.Length == 0)
527 | {
528 | yield return (page.Title, (i, 0, 0));
529 | continue;
530 | }
531 |
532 | var lines = GetNonEmptyLines(page.Text).ToList();
533 | bool isFirst = true;
534 | foreach (var match in FindMatchingLocations(lines, re))
535 | {
536 | var (uiLine, (pos, len)) = match;
537 | if (isFirst)
538 | {
539 | if (options.HasFlag(SearchOptions.TitleLinkToFirstResult))
540 | {
541 | yield return (page.Title, (i, pos, len));
542 | }
543 | else
544 | {
545 | yield return (page.Title, (i, 0, 0));
546 | }
547 | isFirst = false;
548 | }
549 | yield return (" " + uiLine, (i, pos, len));
550 | }
551 | }
552 | }
553 |
554 | // starting at position-1, keep going backwards until test fails
555 | private string GetStringBackwards(string text, int position, Predicate test)
556 | {
557 | if (position == 0)
558 | return "";
559 | if (position > text.Length)
560 | position = text.Length;
561 | int start = position - 1;
562 | while (start >= 0 && start < text.Length && test(text[start]))
563 | --start;
564 | if (position - start == 0)
565 | return "";
566 | return text.Substring(start + 1, position - start - 1);
567 | }
568 |
569 | private void GetTitleCompletions(ScratchBook book, Predicate test, Action add)
570 | {
571 | book.TitleCache.EnumerateValues(value => GetTextCompletions(value, test, add));
572 | }
573 |
574 | private void GetTextCompletions(string text, Predicate test, Action add)
575 | {
576 | int start = -1;
577 | for (int i = 0; i < text.Length; ++i)
578 | {
579 | if (test(text[i]))
580 | {
581 | if (start < 0)
582 | start = i;
583 | }
584 | else if (start >= 0)
585 | {
586 | add(text.Substring(start, i - start));
587 | start = -1;
588 | }
589 | }
590 | if (start > 0)
591 | add(text.Substring(start, text.Length - start));
592 | }
593 |
594 | // TODO: consider getting completions in a different order; e.g. working backwards from a position
595 | private List GetCompletions(ScratchBook book, string text, Predicate test)
596 | {
597 | var unique = new HashSet();
598 | var result = new List();
599 | void add(string candidate)
600 | {
601 | if (!unique.Contains(candidate))
602 | {
603 | unique.Add(candidate);
604 | result.Add(candidate);
605 | }
606 | }
607 |
608 | GetTextCompletions(text, test, add);
609 | GetTitleCompletions(book, test, add);
610 |
611 | if (((ScratchScope)book.Scope).GetOrDefault("debug-completion", false))
612 | result.ForEach(Log.Out);
613 | return result;
614 | }
615 |
616 | [TypedAction("check-for-save")]
617 | internal ScratchValue CheckForSave(ExecutionContext context, IList args)
618 | {
619 | // ...
620 | return ScratchValue.Null;
621 | }
622 |
623 | [TypedAction("complete")]
624 | public void CompleteAtPoint(ExecutionContext context, IList args)
625 | {
626 | // Emacs-style complete-at-point
627 | // foo| -> find symbols starting with foo and complete first found (e.g. 'bar')
628 | // foo[bar]| -> after completing [bar], find symbols starting with foo and complete first after 'bar'
629 | // If cursor isn't exactly at the end of a completion, we don't resume; we try from scratch.
630 | // Completion symbols come from all words ([A-Za-z0-9_-]+) in the document.
631 | var page = GetPage(context.View.Book, context.View.CurrentPageIndex);
632 | if (page == null)
633 | return;
634 | string text = context.View.CurrentText;
635 | var state = page.GetViewState(context.View, PageViewState.Create);
636 | var (currentStart, currentEnd) = state.CurrentCompletion.GetValueOrDefault();
637 | var currentPos = context.View.CurrentPosition;
638 | string prefix, suffix;
639 | if (currentStart < currentEnd && currentPos == currentEnd)
640 | {
641 | prefix = GetStringBackwards(text, currentStart, char.IsLetterOrDigit);
642 | suffix = text.Substring(currentStart, currentEnd - currentStart);
643 | }
644 | else
645 | {
646 | prefix = GetStringBackwards(text, currentPos, char.IsLetterOrDigit);
647 | suffix = "";
648 | currentStart = currentPos;
649 | }
650 | List completions = GetCompletions(context.View.Book, text, char.IsLetterOrDigit);
651 |
652 | int currentIndex = completions.IndexOf(prefix + suffix);
653 | if (currentIndex == -1)
654 | return;
655 |
656 | // find the next completion
657 | string nextSuffix = "";
658 | for (int i = (currentIndex + 1) % completions.Count; i != currentIndex; i = (i + 1) % completions.Count)
659 | if (completions[i].StartsWith(prefix))
660 | {
661 | nextSuffix = completions[i].Substring(prefix.Length);
662 | break;
663 | }
664 | if (suffix.Length > 0)
665 | context.View.DeleteTextBackwards(suffix.Length);
666 | context.View.InsertText(nextSuffix);
667 | state.CurrentCompletion = (currentStart, currentStart + nextSuffix.Length);
668 | }
669 |
670 | private ScratchPage GetPage(ScratchBook book, int index)
671 | {
672 | if (index < 0 || index >= book.Pages.Count)
673 | return null;
674 | return book.Pages[index];
675 | }
676 |
677 | [TypedAction("navigate-sigil", ScratchType.String)]
678 | public void NavigateSigil(ExecutionContext context, IList args)
679 | {
680 | string sigil = args[0].StringValue;
681 | context.View.EnsureSaved();
682 | if (context.View.RunSearch(text => TrySearch(context.View.Book, $"^\\s*{Regex.Escape(sigil)}.*{text}",
683 | SearchOptions.TitleLinkToFirstResult).Take(50), out var triple))
684 | {
685 | var (page, pos, len) = triple;
686 | context.View.JumpToPage(page);
687 | // these should probably be part of JumpToPage, to avoid the default action
688 | context.View.ScrollIntoView(pos);
689 | context.View.Selection = (pos, pos + len);
690 | }
691 | }
692 |
693 | private string ResetIndent(string text)
694 | {
695 | string[] lines = text.Split('\r', '\n');
696 | int minIndent = int.MaxValue;
697 | // TODO: make this tab-aware
698 | foreach (string line in lines)
699 | {
700 | int indent = GetWhitespace(line, 0, line.Length).Length;
701 | // don't count empty lines, or lines with only whitespace
702 | if (indent == line.Length)
703 | continue;
704 | minIndent = Math.Min(minIndent, indent);
705 | }
706 | if (minIndent == 0)
707 | return text;
708 | for (int i = 0; i < lines.Length; ++i)
709 | if (minIndent >= lines[i].Length)
710 | lines[i] = "";
711 | else
712 | lines[i] = lines[i].Substring(minIndent);
713 | return string.Join("\n", lines);
714 | }
715 |
716 | enum IndentOptions
717 | {
718 | None,
719 | SkipFirst,
720 | SkipTrailingEmpty
721 | }
722 |
723 | private string AddIndent(string indent, string text, IndentOptions options = IndentOptions.None)
724 | {
725 | string[] lines = text.Split(new[] { '\r', '\n' }, StringSplitOptions.None);
726 | int firstLine = options == IndentOptions.SkipFirst ? 1 : 0;
727 | int lastLine = lines.Length - 1;
728 | if (lastLine >= 0 && options == IndentOptions.SkipTrailingEmpty && string.IsNullOrEmpty(lines[lastLine]))
729 | --lastLine;
730 | for (int i = firstLine; i <= lastLine; ++i)
731 | {
732 | lines[i] = indent + lines[i];
733 | }
734 | return string.Join("\n", lines);
735 | }
736 |
737 | // Gets the position of the character which ends the line.
738 | private int GetLineEnd(string text, int position)
739 | {
740 | while (position < text.Length)
741 | {
742 | switch (text[position])
743 | {
744 | case '\r':
745 | case '\n':
746 | return position;
747 |
748 | default:
749 | ++position;
750 | break;
751 | }
752 | }
753 | return position;
754 | }
755 |
756 | // Extract all whitespace from text[position] up to least(non-whitespace, max).
757 | private string GetWhitespace(string text, int position, int max)
758 | {
759 | int start = position;
760 | while (position < text.Length && position < max)
761 | {
762 | char ch = text[position];
763 | if (ch == '\r' || ch == '\n')
764 | break;
765 | if (char.IsWhiteSpace(ch))
766 | ++position;
767 | else
768 | break;
769 | }
770 | return text.Substring(start, position - start);
771 | }
772 |
773 | private string GetCurrentIndent(string text, int position)
774 | {
775 | int lineStart = GetLineStart(text, position);
776 | return GetWhitespace(text, lineStart, position);
777 | }
778 |
779 | // Gets the position of the character which starts the line.
780 | private int GetLineStart(string text, int position)
781 | {
782 | if (position == 0)
783 | return 0;
784 | // If we are at the "end" of the line in the editor we are also at the start of the next line
785 | --position;
786 | while (position > 0)
787 | {
788 | switch (text[position])
789 | {
790 | case '\r':
791 | case '\n':
792 | return position + 1;
793 |
794 | default:
795 | --position;
796 | break;
797 | }
798 | }
799 | return position;
800 | }
801 |
802 | private char GetNextNonWhite(string text, ref int pos)
803 | {
804 | while (pos < text.Length && char.IsWhiteSpace(text, pos))
805 | ++pos;
806 | if (pos >= text.Length)
807 | return '\0';
808 | return text[pos];
809 | }
810 |
811 | enum SmartDelimiter
812 | {
813 | Yes,
814 | No,
815 | IndentOnly
816 | }
817 |
818 | private SmartDelimiter IsSmartDelimiter(string text, int pos, out char closer)
819 | {
820 | closer = ' ';
821 | if (pos < 0 || pos >= text.Length)
822 | return SmartDelimiter.No;
823 | int smartIndex = Array.IndexOf(SmartChars, text[pos]);
824 | if (smartIndex < 0)
825 | return SmartDelimiter.No;
826 | closer = SmartInversion[smartIndex];
827 | int currIndent = GetIndent(GetCurrentIndent(text, pos));
828 | ++pos;
829 | char nextCh = GetNextNonWhite(text, ref pos);
830 | int nextIndent = GetIndent(GetCurrentIndent(text, pos));
831 | // foo(|<-- end of file; smart delimiter
832 | if (nextCh == '\0')
833 | return SmartDelimiter.Yes;
834 | // foo(|<-- next indent is equal indent but not a match; new delimeter
835 | // blah
836 | if (currIndent == nextIndent && nextCh != closer)
837 | return SmartDelimiter.Yes;
838 | // foo (
839 | // ..blah (|<-- next indent is less indented; we want a new delimeter
840 | // )
841 | //
842 | // foo (|<-- next indent is equal indent; no new delimiter but do indent
843 | // )
844 | //
845 | // foo(|<-- next indent is more indented; no new delimeter but do indent
846 | // abc()
847 | // )
848 | if (nextIndent < currIndent)
849 | return SmartDelimiter.Yes;
850 | return SmartDelimiter.IndentOnly;
851 | }
852 |
853 |
854 | }
855 |
856 | }
--------------------------------------------------------------------------------
/gtk-ui/Scratch.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Text;
4 | using System.Linq;
5 | using System.Collections.ObjectModel;
6 | using System.IO;
7 | using System.Text.RegularExpressions;
8 | using System.Reflection;
9 | using System.Collections;
10 |
11 | namespace Barrkel.ScratchPad
12 | {
13 | public static class Log
14 | {
15 | public static Action Handler { get; set; } = Console.Error.WriteLine;
16 |
17 | public static void Out(string line)
18 | {
19 | Handler(line);
20 | }
21 | }
22 |
23 | public interface IScratchScope
24 | {
25 | IScratchScope CreateChild(string name);
26 | }
27 |
28 | public class NullScope : IScratchScope
29 | {
30 | public static readonly IScratchScope Instance = new NullScope();
31 |
32 | private NullScope()
33 | {
34 | }
35 |
36 | public IScratchScope CreateChild(string name)
37 | {
38 | return this;
39 | }
40 | }
41 |
42 | public class Options
43 | {
44 | public Options(List args)
45 | {
46 | NormalizeFiles = ParseFlag(args, "normalize");
47 | }
48 |
49 | static bool MatchFlag(string arg, string name)
50 | {
51 | return arg == "--" + name;
52 | }
53 |
54 | static bool ParseFlag(List args, string name)
55 | {
56 | return args.RemoveAll(arg => MatchFlag(arg, name)) > 0;
57 | }
58 |
59 | public bool NormalizeFiles { get; }
60 | }
61 |
62 | // The main root. Every directory in this directory is a tab on the main interface, like a separate
63 | // notebook.
64 | // Every file in this directory is in the main notebook.
65 | public class ScratchRoot
66 | {
67 | readonly List _books;
68 |
69 | public ScratchRoot(Options options, string rootDirectory, IScratchScope rootScope)
70 | {
71 | RootScope = rootScope;
72 | Options = options;
73 | _books = new List();
74 | Books = new ReadOnlyCollection(_books);
75 | RootDirectory = rootDirectory;
76 | _books.Add(new ScratchBook(this, rootDirectory));
77 | foreach (string dir in Directory.GetDirectories(rootDirectory))
78 | _books.Add(new ScratchBook(this, dir));
79 | }
80 |
81 | public Options Options { get; }
82 |
83 | public string RootDirectory { get; }
84 |
85 | public ReadOnlyCollection Books { get; }
86 |
87 | public IScratchScope RootScope { get; }
88 |
89 | public void SaveLatest()
90 | {
91 | foreach (var book in Books)
92 | book.SaveLatest();
93 | }
94 | }
95 |
96 | static class DictionaryExtension
97 | {
98 | public static void Deconstruct(this KeyValuePair self, out TKey key, out TValue value)
99 | {
100 | key = self.Key;
101 | value = self.Value;
102 | }
103 | }
104 |
105 | // Simple text key to text value cache with timestamp-based invalidation.
106 | public class LineCache
107 | {
108 | string _storeFile;
109 | Dictionary _cache = new Dictionary();
110 | bool _dirty;
111 |
112 | public LineCache(string storeFile)
113 | {
114 | _storeFile = storeFile;
115 | Load();
116 | }
117 |
118 | private void Load()
119 | {
120 | _cache.Clear();
121 | if (!File.Exists(_storeFile))
122 | return;
123 | using (var r = new LineReader(_storeFile))
124 | {
125 | int count = int.Parse(r.ReadLine());
126 | for (int i = 0; i < count; ++i)
127 | {
128 | string name = StringUtil.Unescape(r.ReadLine());
129 | // This is parsing UTC but it doesn't return UTC!
130 | DateTime timestamp = DateTime.Parse(r.ReadLine()).ToUniversalTime();
131 | string line = StringUtil.Unescape(r.ReadLine());
132 | _cache[name] = (timestamp, line);
133 | }
134 | }
135 | }
136 |
137 | public void Save()
138 | {
139 | if (!_dirty)
140 | return;
141 | using (var w = new LineWriter(_storeFile, FileMode.Create))
142 | {
143 | w.WriteLine(_cache.Count.ToString());
144 | foreach (var (name, (timestamp, line)) in _cache)
145 | {
146 | w.WriteLine(StringUtil.Escape(name));
147 | w.WriteLine(timestamp.ToString("o"));
148 | w.WriteLine(StringUtil.Escape(line));
149 | }
150 | }
151 | _dirty = false;
152 | }
153 |
154 | public string Get(string name, DateTime timestamp, Func fetch)
155 | {
156 | if (_cache.TryGetValue(name, out var entry))
157 | {
158 | var (cacheTs, line) = entry;
159 | TimeSpan age = timestamp - cacheTs;
160 | // 1 second leeway because of imprecision in file system timestamps etc.
161 | if (age < TimeSpan.FromSeconds(1))
162 | return line;
163 | }
164 | var update = fetch();
165 | Put(name, timestamp, update);
166 | return update;
167 | }
168 |
169 | public void Put(string name, DateTime timestamp, string line)
170 | {
171 | _cache[name] = (timestamp, line);
172 | _dirty = true;
173 | }
174 |
175 | public void EnumerateValues(Action callback)
176 | {
177 | foreach (var (_, line) in _cache.Values)
178 | callback(line);
179 | }
180 | }
181 |
182 | public class ScratchBook
183 | {
184 | List _pages = new List();
185 | string _rootDirectory;
186 |
187 | public ScratchBook(ScratchRoot root, string rootDirectory)
188 | {
189 | Root = root;
190 | _rootDirectory = rootDirectory;
191 | Scope = root.RootScope.CreateChild(Name);
192 | Pages = new ReadOnlyCollection(_pages);
193 | var rootDir = new DirectoryInfo(rootDirectory);
194 | TitleCache = new LineCache(Path.Combine(_rootDirectory, "title_cache.text"));
195 | _pages.AddRange(rootDir.GetFiles("*.txt")
196 | .Union(rootDir.GetFiles("*.log"))
197 | .OrderBy(f => f.LastWriteTimeUtc)
198 | .Select(f => Path.ChangeExtension(f.FullName, null))
199 | .Distinct()
200 | .Select(name => new ScratchPage(TitleCache, name)));
201 | }
202 |
203 | public ScratchRoot Root { get; }
204 |
205 | public IScratchScope Scope { get; }
206 |
207 | internal LineCache TitleCache { get; }
208 |
209 | static bool DoesBaseNameExist(string baseName)
210 | {
211 | string logFile = Path.ChangeExtension(baseName, ".log");
212 | string textFile = Path.ChangeExtension(baseName, ".txt");
213 | return File.Exists(logFile) || File.Exists(textFile);
214 | }
215 |
216 | string FindNewPageBaseName()
217 | {
218 | string now = DateTime.UtcNow.ToString("yyyy-MM-dd_HH-mm");
219 |
220 | string stem = string.Format("page-{0}", now);
221 | string result = Path.Combine(_rootDirectory, stem);
222 | if (!DoesBaseNameExist(result))
223 | return result;
224 |
225 | for (int i = 1; ; ++i)
226 | {
227 | result = Path.Combine(_rootDirectory,
228 | string.Format("{0}-{1:000}", stem, i));
229 | if (!DoesBaseNameExist(result))
230 | return result;
231 | }
232 | }
233 |
234 | public ReadOnlyCollection Pages { get; }
235 |
236 | public int MoveToEnd(int pageIndex)
237 | {
238 | var page = _pages[pageIndex];
239 | _pages.RemoveAt(pageIndex);
240 | _pages.Add(page);
241 | return _pages.Count - 1;
242 | }
243 |
244 | public ScratchPage AddPage()
245 | {
246 | ScratchPage result = new ScratchPage(TitleCache, FindNewPageBaseName());
247 | _pages.Add(result);
248 | return result;
249 | }
250 |
251 | // This is called periodically, and on every modification.
252 | public void EnsureSaved()
253 | {
254 | TitleCache.Save();
255 | }
256 |
257 | // This is only called if content has been modified.
258 | public void SaveLatest()
259 | {
260 | foreach (var page in Pages)
261 | page.SaveLatest();
262 | }
263 |
264 | static bool IsSearchMatch(string text, string[] parts)
265 | {
266 | foreach (string part in parts)
267 | {
268 | switch (part[0])
269 | {
270 | case '-':
271 | {
272 | string neg = part.Substring(1);
273 | if (neg.Length == 0)
274 | continue;
275 | if (text.IndexOf(neg, StringComparison.InvariantCultureIgnoreCase) >= 0)
276 | return false;
277 |
278 | break;
279 | }
280 |
281 | default:
282 | if (text.IndexOf(part, StringComparison.InvariantCultureIgnoreCase) < 0)
283 | return false;
284 | break;
285 | }
286 | }
287 |
288 | return true;
289 | }
290 |
291 | public IEnumerable<(string, int)> SearchTitles(string text)
292 | {
293 | string[] parts = text.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
294 |
295 | for (int i = Pages.Count - 1; i >= 0; --i)
296 | {
297 | var page = Pages[i];
298 |
299 | if (IsSearchMatch(page.Title, parts))
300 | yield return (page.Title, i);
301 | }
302 | }
303 |
304 | public IEnumerable<(string, int)> SearchText(Regex re)
305 | {
306 | for (int i = Pages.Count - 1; i >= 0; --i)
307 | {
308 | var page = Pages[i];
309 | if (re.Match(page.Text).Success)
310 | yield return (page.Title, i);
311 | }
312 | }
313 |
314 | ///
315 | /// Returns (title, index) of each match
316 | ///
317 | public IEnumerable<(string, int)> SearchTitles(Regex re)
318 | {
319 | for (int i = Pages.Count - 1; i >= 0; --i)
320 | {
321 | var page = Pages[i];
322 | if (re.Match(page.Title).Success)
323 | yield return (page.Title, i);
324 | }
325 | }
326 |
327 | public string Name => Path.GetFileName(_rootDirectory);
328 |
329 | public override string ToString()
330 | {
331 | return Name;
332 | }
333 | }
334 |
335 | interface IReadOnlyPage
336 | {
337 | DateTime ChangeStamp { get; }
338 | string Title { get; }
339 | string Text { get; }
340 | }
341 |
342 | public class ScratchPage
343 | {
344 | LiteScratchPage _liteImpl;
345 | RealScratchPage _realImpl;
346 | // These timestamps are for detecting out of process modifications to txt and log files
347 | DateTime? _logStamp;
348 | DateTime? _textStamp;
349 | string _baseName;
350 | string _shortName;
351 | LineCache _titleCache;
352 | Dictionary