Skip to content

Commit 5db2d65

Browse files
committed
add support for glob syntax
See documentation for Glob class in Glob.cs Note that this is fully backwards-compatible with existing non-fuzzy matching, so it is unnecessary to add a new setting. Moved some helper functions to MiscUtils.cs Rearranged code in NppNavigateTo.cs in an order that more closely parallels the order in which it is first referenced. Also started to add code for a background worker. I may eventually consider offloading some work to a background worker but currently it doesn't seem super necessary.
1 parent 2b693f9 commit 5db2d65

File tree

9 files changed

+830
-212
lines changed

9 files changed

+830
-212
lines changed

NppNavigateTo/Forms/AboutForm.cs

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Reflection;
99
using System.Text;
1010
using System.Windows.Forms;
11+
using NppPluginNET;
1112

1213
namespace NavigateTo.Plugin.Namespace
1314
{
@@ -16,7 +17,7 @@ public partial class AboutForm : Form
1617
public AboutForm()
1718
{
1819
InitializeComponent();
19-
Title.Text = $"NavigateTo v{AssemblyVersionString()}";
20+
Title.Text = $"NavigateTo v{MiscUtils.AssemblyVersionString()}";
2021
FormStyle.ApplyStyle(this, true, Main.notepad.IsDarkModeEnabled());
2122
}
2223

@@ -44,14 +45,6 @@ private void GitHubLink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs
4445
}
4546
}
4647

47-
public static string AssemblyVersionString()
48-
{
49-
string version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
50-
while (version.EndsWith(".0"))
51-
version = version.Substring(0, version.Length - 2);
52-
return version;
53-
}
54-
5548
/// <summary>
5649
/// Escape key exits the form.
5750
/// </summary>

NppNavigateTo/Forms/FrmNavigateTo.cs

Lines changed: 232 additions & 203 deletions
Large diffs are not rendered by default.

NppNavigateTo/Glob.cs

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
using Kbg.NppPluginNET.PluginInfrastructure;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Text.RegularExpressions;
7+
using System.Windows.Forms;
8+
9+
namespace NavigateTo.Plugin.Namespace
10+
{
11+
public enum Ternary
12+
{
13+
FALSE,
14+
TRUE,
15+
UNDECIDED,
16+
}
17+
18+
public class GlobFunction
19+
{
20+
public Func<string, bool> Matcher { get; }
21+
public bool Or { get; }
22+
public bool Negated { get; }
23+
24+
public GlobFunction(Func<string, bool> matcher, bool or, bool negated)
25+
{
26+
this.Matcher = matcher;
27+
this.Or = or;
28+
this.Negated = negated;
29+
}
30+
31+
public Ternary IsMatch(string inp, Ternary previousResult)
32+
{
33+
if (Or && (previousResult == Ternary.TRUE))
34+
return Ternary.TRUE;
35+
if (!Or && (previousResult == Ternary.FALSE))
36+
return Ternary.FALSE;
37+
bool result = Matcher(inp);
38+
return (result ^ Negated) ? Ternary.TRUE : Ternary.FALSE;
39+
}
40+
}
41+
42+
/// <summary>
43+
/// parser that parses glob syntax in space-separated globs<br></br>
44+
/// 1. Default behavior is to match ALL space-separated globs
45+
/// (e.g. "foo bar txt" matches "foobar.txt" and "bartxt.foo")<br></br>
46+
/// 2. Also use glob syntax:<br></br>
47+
/// * "*" matches any number (incl. zero) of characters except "\\"<br></br>
48+
/// * "**" matches any number (incl. zero) of characters including "\\"<br></br>
49+
/// * "[chars]" matches all characters inside the square brackets (same as in Perl-style regex, also includes character classes like [a-z], [0-9]). Note: can match ' ', which is normally a glob separator<br></br>
50+
/// * "[!chars]" matches NONE of the characters inside the square brackets (and also doesn't match "\\") (same as Perl-style "[^chars]")<br></br>
51+
/// * "foo.{abc,def,hij}" matches "foo.abc", "foo.def", or "foo.hij". Essentially "{x,y,z}" is equivalent to "(?:x|y|z)" in Perl-style regex<br></br>
52+
/// * "?" matches any one character except "\\"<br></br>
53+
/// 3. "foo | bar" matches foo OR bar ("|" implements logical OR)<br></br>
54+
/// 4. "!foo" matches anything that DOES NOT CONTAIN "foo"<br></br>
55+
/// 5. "foo | &lt;baz bar&gt;" matches foo OR (bar AND baz) (that is, "&lt;" and "&gt;" act as grouping parentheses)
56+
/// </summary>
57+
public class Glob
58+
{
59+
public int ii;
60+
public string ErrorMsg;
61+
public int ErrorPos;
62+
public List<string> globs;
63+
64+
public Glob()
65+
{
66+
globs = new List<string>();
67+
Reset();
68+
}
69+
70+
public void Reset()
71+
{
72+
globs.Clear();
73+
ErrorMsg = null;
74+
ErrorPos = -1;
75+
ii = 0;
76+
}
77+
78+
public char Peek(string inp)
79+
{
80+
return (ii < inp.Length - 1)
81+
? inp[ii + 1]
82+
: '\x00';
83+
}
84+
85+
public Regex Glob2Regex(string inp)
86+
{
87+
var sb = new StringBuilder();
88+
int start = ii;
89+
bool is_char_class = false;
90+
bool uses_metacharacters = false;
91+
bool is_alternation = false;
92+
while (ii < inp.Length)
93+
{
94+
char c = inp[ii];
95+
char next_c;
96+
switch (c)
97+
{
98+
case '/': sb.Append("\\\\"); break;
99+
case '\\':
100+
next_c = Peek(inp);
101+
if (next_c == 'x' || next_c == 'u' // \xNN, \uNNNN unicode escapes
102+
|| (next_c == ']' && is_char_class))
103+
sb.Append('\\');
104+
else
105+
sb.Append("\\\\");
106+
break;
107+
case '*':
108+
if (is_char_class)
109+
{
110+
sb.Append("\\*"); // "[*]" matches literal * character
111+
break;
112+
}
113+
else if (sb.Length == 0)
114+
break; // since globs are only anchored at the end,
115+
// leading * in globs should not influence the matching behavior.
116+
// For example, the globs "*foo.txt" and "foo.txt" should match the same things.
117+
uses_metacharacters = true;
118+
next_c = Peek(inp);
119+
if (next_c == '*')
120+
{
121+
ii++;
122+
// "**" means recursive search; ignores path delimiters
123+
sb.Append(".*");
124+
}
125+
else
126+
{
127+
// "*" means anything but a path delimiter
128+
sb.Append(@"[^\\]*");
129+
}
130+
break;
131+
case '[':
132+
uses_metacharacters = true;
133+
sb.Append('[');
134+
next_c = Peek(inp);
135+
if (!is_char_class && next_c == '!')
136+
{
137+
sb.Append("^\\\\"); // [! begins a negated char class, but also need to exclude path sep
138+
ii++;
139+
}
140+
is_char_class = true;
141+
break;
142+
case ']':
143+
is_char_class = false;
144+
sb.Append(']');
145+
break; // TODO: consider allowing nested [] inside char class
146+
case '{':
147+
if (is_char_class)
148+
sb.Append("\\{");
149+
else
150+
{
151+
uses_metacharacters = true;
152+
is_alternation = true; // e.g. *.{cpp,h,c} matches *.cpp or *.h or *.c
153+
sb.Append("(?:");
154+
}
155+
break;
156+
case '}':
157+
if (is_char_class)
158+
sb.Append("\\}");
159+
else
160+
{
161+
sb.Append(")");
162+
is_alternation = false;
163+
}
164+
break;
165+
case ',':
166+
if (is_alternation)
167+
sb.Append('|'); // e.g. *.{cpp,h,c} matches *.cpp or *.h or *.c
168+
else
169+
sb.Append(',');
170+
break;
171+
case '.': case '$': case '(': case ')': case '^':
172+
// these chars have no special meaning in glob syntax, but they're regex metacharacters
173+
sb.Append('\\');
174+
sb.Append(c);
175+
break;
176+
case '?':
177+
if (is_char_class)
178+
sb.Append('?');
179+
else
180+
{
181+
uses_metacharacters = true;
182+
sb.Append(@"[^\\]"); // '?' is any single char
183+
}
184+
break;
185+
case '|': case '<': case '>': case ';': case '\t':
186+
goto endOfLoop; // these characters are never allowed in Windows paths
187+
case ' ': case '!':
188+
if (is_char_class)
189+
sb.Append(c);
190+
else
191+
goto endOfLoop; // allow ' ' and '!' inside char classes, but otherwise these are special chars in NavigateTo
192+
break;
193+
default:
194+
sb.Append(c);
195+
break;
196+
}
197+
ii++;
198+
}
199+
endOfLoop:
200+
if (uses_metacharacters) // anything without "*" or "?" or "[]" or
201+
sb.Append('$'); // globs are anchored at the end; that is "*foo" does not match "foo/bar.txt" but "*foo.tx?" does
202+
string pat = sb.ToString();
203+
try
204+
{
205+
var regex = new Regex(pat, RegexOptions.IgnoreCase | RegexOptions.Compiled);
206+
globs.Add(pat);
207+
return regex;
208+
}
209+
catch (Exception ex)
210+
{
211+
ErrorMsg = ex.Message;
212+
ErrorPos = start;
213+
return null;
214+
}
215+
}
216+
217+
public Func<string, bool> Parse(string inp, bool fromBeginning = true)
218+
{
219+
if (fromBeginning)
220+
Reset();
221+
bool or = false;
222+
bool negated = false;
223+
var globFuncs = new List<GlobFunction>();
224+
while (ii < inp.Length)
225+
{
226+
char c = inp[ii];
227+
if (c == ' ' || c == '\t' || c == ';') { }
228+
else if (c == '|')
229+
or = true;
230+
else if (c == '!')
231+
negated = !negated;
232+
else if (c == '<')
233+
{
234+
ii++;
235+
var subFunc = Parse(inp, false);
236+
globFuncs.Add(new GlobFunction(subFunc, or, negated));
237+
negated = false;
238+
or = false;
239+
}
240+
else if (c == '>')
241+
break;
242+
else
243+
{
244+
var globRegex = Glob2Regex(inp);
245+
if (globRegex == null)
246+
continue; // ignore errors, try to parse everything else
247+
globFuncs.Add(new GlobFunction(globRegex.IsMatch, or, negated));
248+
ii--;
249+
negated = false;
250+
or = false;
251+
}
252+
ii++;
253+
}
254+
ii++;
255+
if (globFuncs.All(gf => !gf.Or))
256+
// return a more efficient function that short-circuits
257+
// if none of the GlobFunctions use logical or
258+
return (string x) => globFuncs.All(gf => gf.Matcher(x) ^ gf.Negated);
259+
bool finalFunc(string x)
260+
{
261+
Ternary result = Ternary.UNDECIDED;
262+
foreach (GlobFunction globFunc in globFuncs)
263+
{
264+
result = globFunc.IsMatch(x, result);
265+
}
266+
return result != Ternary.FALSE;
267+
}
268+
return finalFunc;
269+
}
270+
}
271+
}

NppNavigateTo/MiscUtils.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System;
2+
using System.Reflection;
3+
using System.Text;
4+
using System.Windows.Forms;
5+
using Kbg.NppPluginNET.PluginInfrastructure;
6+
7+
namespace NppPluginNET
8+
{
9+
/// <summary>
10+
/// contains connectors to Scintilla (editor) and Notepad++ (notepad)
11+
/// and miscellaneous other helper functions
12+
/// </summary>
13+
public class MiscUtils
14+
{
15+
/// <summary>
16+
/// connector to Scintilla
17+
/// </summary>
18+
public static IScintillaGateway editor = NavigateTo.Plugin.Namespace.Main.editor;
19+
/// <summary>
20+
/// connector to Notepad++
21+
/// </summary>
22+
public static INotepadPPGateway notepad = NavigateTo.Plugin.Namespace.Main.notepad;
23+
24+
/// <summary>
25+
/// append text to current doc, then append newline and move cursor
26+
/// </summary>
27+
/// <param name="inp"></param>
28+
public static void AddLine(string inp)
29+
{
30+
editor.AppendText(Encoding.UTF8.GetByteCount(inp), inp);
31+
editor.AppendText(Environment.NewLine.Length, Environment.NewLine);
32+
}
33+
34+
/// <summary>
35+
/// input is one of 'p', 'd', 'f'<br></br>
36+
/// if 'p', get full path to current file (default)<br></br>
37+
/// if 'd', get directory of current file<br></br>
38+
/// if 'f', get filename of current file
39+
/// </summary>
40+
/// <param name="which"></param>
41+
/// <returns></returns>
42+
public static string GetCurrentPath(char which = 'p')
43+
{
44+
NppMsg msg = NppMsg.NPPM_GETFULLCURRENTPATH;
45+
switch (which)
46+
{
47+
case 'p': break;
48+
case 'd': msg = NppMsg.NPPM_GETCURRENTDIRECTORY; break;
49+
case 'f': msg = NppMsg.NPPM_GETFILENAME; break;
50+
default: throw new ArgumentException("GetCurrentPath argument must be one of 'p', 'd', 'f'");
51+
}
52+
53+
StringBuilder path = new StringBuilder(Win32.MAX_PATH);
54+
Win32.SendMessage(PluginBase.nppData._nppHandle, (uint)msg, 0, path);
55+
56+
return path.ToString();
57+
}
58+
59+
/// <summary>
60+
/// Trying to copy an empty string or null to the clipboard raises an error.<br></br>
61+
/// This shows a message box if the user tries to do that.
62+
/// </summary>
63+
/// <param name="text"></param>
64+
public static void TryCopyToClipboard(string text)
65+
{
66+
if (text == null || text.Length == 0)
67+
{
68+
MessageBox.Show("Couldn't find anything to copy to the clipboard",
69+
"Nothing to copy to clipboard",
70+
MessageBoxButtons.OK,
71+
MessageBoxIcon.Warning
72+
);
73+
return;
74+
}
75+
Clipboard.SetText(text);
76+
}
77+
78+
public static string AssemblyVersionString()
79+
{
80+
string version = Assembly.GetExecutingAssembly().GetName().Version.ToString();
81+
while (version.EndsWith(".0"))
82+
version = version.Substring(0, version.Length - 2);
83+
return version;
84+
}
85+
}
86+
}

0 commit comments

Comments
 (0)