Skip to content

Commit 7f82ae6

Browse files
Merge pull request #51 from molsonkiko/glob_syntax
add support for glob syntax, address issue 50
2 parents 2b693f9 + 5db2d65 commit 7f82ae6

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)