Skip to content

Commit 42e3ede

Browse files
authored
Improve syntax highlighting for bash/pwsh and add the parameter -streaming to /render (#149)
1 parent c660c55 commit 42e3ede

File tree

4 files changed

+219
-21
lines changed

4 files changed

+219
-21
lines changed

shell/Markdown.VT/ColorCode.VT/Parser/Bash.cs

+17-17
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class Bash : ILanguage
1818
new List<LanguageRule>
1919
{
2020
new LanguageRule(
21-
@"(\#.*?)\r?$",
21+
@"(#.*?)\r?$",
2222
new Dictionary<int, string>
2323
{
2424
{1, BashCommentScope}
@@ -34,30 +34,30 @@ public class Bash : ILanguage
3434

3535
// match options like '-word'
3636
new LanguageRule(
37-
@"\s+-\w+",
37+
@"\s(-\w+)",
3838
new Dictionary<int, string>
39-
{
40-
{0, ScopeName.PowerShellParameter}
41-
}
42-
),
39+
{
40+
{1, ScopeName.PowerShellParameter}
41+
}),
4342

44-
// match options like '--word', '--word-word', '--word-word-word' and etc.
43+
// match options like '--word', '--word-word', and '--word-word-word', but not '--word-' or '--word-word-'.
44+
// Also match potential value for the option that is specified in the form of '--word=value', but we don't
45+
// capture the value part because it should be rendered as plain text, and our real purpose is to not let
46+
// the value part to be matched by other rules.
4547
new LanguageRule(
46-
@"\s+--(?:\w+-?)+",
48+
@"\s(--(?:\w+-)*\w+)(?:=(?:\w+-)*\w+)?",
4749
new Dictionary<int, string>
48-
{
49-
{0, ScopeName.PowerShellParameter}
50-
}
51-
),
50+
{
51+
{1, ScopeName.PowerShellParameter}
52+
}),
5253

5354
// match variable like '$word', '$digit', '$word_word' and etc.
5455
new LanguageRule(
55-
@"\$(?:[\d\w]+_?)+",
56+
@"\$\w+",
5657
new Dictionary<int, string>
57-
{
58-
{0, ScopeName.PowerShellVariable}
59-
}
60-
),
58+
{
59+
{0, ScopeName.PowerShellVariable}
60+
}),
6161
};
6262

6363
public bool HasAlias(string lang)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using ColorCode.Common;
5+
6+
namespace ColorCode.VT;
7+
8+
public class PowerShell : ILanguage
9+
{
10+
public string Id
11+
{
12+
get { return LanguageId.PowerShell; }
13+
}
14+
15+
public string Name
16+
{
17+
get { return "PowerShell"; }
18+
}
19+
20+
public string CssClassName
21+
{
22+
get { return "powershell"; }
23+
}
24+
25+
public string FirstLinePattern
26+
{
27+
get { return null; }
28+
}
29+
30+
public IList<LanguageRule> Rules
31+
{
32+
get
33+
{
34+
return new List<LanguageRule>
35+
{
36+
new LanguageRule(
37+
@"(?s)(<#.*?#>)",
38+
new Dictionary<int, string>
39+
{
40+
{1, ScopeName.Comment}
41+
}),
42+
43+
new LanguageRule(
44+
@"(#.*?)\r?$",
45+
new Dictionary<int, string>
46+
{
47+
{1, ScopeName.Comment}
48+
}),
49+
50+
new LanguageRule(
51+
@"'[^\n]*?(?<!\\)'",
52+
new Dictionary<int, string>
53+
{
54+
{0, ScopeName.String}
55+
}),
56+
57+
new LanguageRule(
58+
@"(?s)@"".*?""@",
59+
new Dictionary<int, string>
60+
{
61+
{0, ScopeName.StringCSharpVerbatim}
62+
}),
63+
64+
new LanguageRule(
65+
@"(?s)(""[^\n]*?(?<!`)"")",
66+
new Dictionary<int, string>
67+
{
68+
{0, ScopeName.String}
69+
}),
70+
71+
new LanguageRule(
72+
@"\$(?:[\d\w\-]+(?::[\d\w\-]+)?|\$|\?|\^)",
73+
new Dictionary<int, string>
74+
{
75+
{0, ScopeName.PowerShellVariable}
76+
}),
77+
78+
new LanguageRule(
79+
@"\${[^}]+}",
80+
new Dictionary<int, string>
81+
{
82+
{0, ScopeName.PowerShellVariable}
83+
}),
84+
85+
new LanguageRule(
86+
@"(?i)\b(begin|break|catch|continue|data|do|dynamicparam|elseif|else|end|exit|filter|finally|foreach|for|from|function|if|in|param|process|return|switch|throw|trap|try|until|while)\b",
87+
new Dictionary<int, string>
88+
{
89+
{1, ScopeName.Keyword}
90+
}),
91+
92+
// We use positive lookbehind assertion to indicate what should immediately precedes a command name.
93+
// By using the positive lookbehind assertion, the operators like '|' and '=' can still be matched
94+
// by their respective rules.
95+
new LanguageRule(
96+
@"(?<=(?:^|[\(\|=])\s*)(?:\w+-)*\w+",
97+
new Dictionary<int, string>
98+
{
99+
{0, ScopeName.PowerShellCommand}
100+
}),
101+
102+
new LanguageRule(
103+
@"-(?:c|i)?(?:eq|ne|gt|ge|lt|le|notlike|like|notmatch|match|notcontains|contains|replace)",
104+
new Dictionary<int, string>
105+
{
106+
{0, ScopeName.PowerShellOperator}
107+
}),
108+
109+
new LanguageRule(
110+
@"-(?:band|and|as|join|not|bxor|xor|bor|or|isnot|is|split)",
111+
new Dictionary<int, string>
112+
{
113+
{0, ScopeName.PowerShellOperator}
114+
}),
115+
116+
// Match parameters like '-word'. Note that we require a preceding whitespace.
117+
new LanguageRule(
118+
@"\s(-\w+)",
119+
new Dictionary<int, string>
120+
{
121+
{1, ScopeName.PowerShellParameter}
122+
}),
123+
124+
// match options like '--word', '--word-word', and '--word-word-word', but not '--word-' or '--word-word-'.
125+
// Also match potential value for the option that is specified in the form of '--word=value', but we don't
126+
// capture the value part because it should be rendered as plain text, and our real purpose is to not let
127+
// the value part to be matched by other rules.
128+
new LanguageRule(
129+
@"\s(--(?:\w+-)*\w+)(?:=(?:\w+-)*\w+)?",
130+
new Dictionary<int, string>
131+
{
132+
{1, ScopeName.PowerShellParameter}
133+
}),
134+
135+
new LanguageRule(
136+
@"(?:\+=|-=|\*=|/=|%=|=|\+\+|--|\+|-|\*|/|%|\||,)",
137+
new Dictionary<int, string>
138+
{
139+
{0, ScopeName.PowerShellOperator}
140+
}),
141+
142+
new LanguageRule(
143+
@"(?:\>\>|2\>&1|\>|2\>\>|2\>)",
144+
new Dictionary<int, string>
145+
{
146+
{0, ScopeName.PowerShellOperator}
147+
}),
148+
149+
new LanguageRule(
150+
@"(?is)\[(cmdletbinding|alias|outputtype|parameter|validatenotnull|validatenotnullorempty|validatecount|validateset|allownull|allowemptycollection|allowemptystring|validatescript|validaterange|validatepattern|validatelength|supportswildcards)[^\]]+\]",
151+
new Dictionary<int, string>
152+
{
153+
{1, ScopeName.PowerShellAttribute}
154+
}),
155+
156+
new LanguageRule(
157+
@"(\[)([^\]]+)(\])(::)?",
158+
new Dictionary<int, string>
159+
{
160+
{1, ScopeName.PowerShellOperator},
161+
{2, ScopeName.PowerShellType},
162+
{3, ScopeName.PowerShellOperator},
163+
{4, ScopeName.PowerShellOperator}
164+
})
165+
};
166+
}
167+
}
168+
169+
public bool HasAlias(string lang)
170+
{
171+
switch (lang.ToLower())
172+
{
173+
case "posh":
174+
case "ps1":
175+
case "pwsh":
176+
return true;
177+
178+
default:
179+
return false;
180+
}
181+
}
182+
}

shell/Markdown.VT/ColorCode.VT/VTSyntaxHighlighter.cs

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ public VTSyntaxHighlighter(StyleDictionary styles = null, ILanguageParser langua
4141

4242
Languages.Load(new Bash());
4343
Languages.Load(new Json());
44+
Languages.Load(new PowerShell());
4445
}
4546

4647
/// <summary>

shell/ShellCopilot.Kernel/Command/RenderCommand.cs

+19-4
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,36 @@ public RenderCommand()
1010
: base("render", "Render a markdown file, for diagnosis purpose.")
1111
{
1212
var file = new Argument<FileInfo>("file", "The file path to save the code to.");
13+
var append = new Option<bool>("--streaming", "Render in the streaming manner.");
14+
1315
AddArgument(file);
14-
this.SetHandler(SaveAction, file);
16+
AddOption(append);
17+
this.SetHandler(SaveAction, file, append);
1518
}
1619

17-
private void SaveAction(FileInfo file)
20+
private void SaveAction(FileInfo file, bool streaming)
1821
{
1922
var host = Shell.Host;
2023

2124
try
2225
{
2326
using FileStream stream = file.OpenRead();
2427
using StreamReader reader = new(stream, Encoding.Default);
25-
2628
string text = reader.ReadToEnd();
27-
host.RenderFullResponse(text);
29+
30+
if (streaming)
31+
{
32+
using var streamingRender = host.NewStreamRender(CancellationToken.None);
33+
string[] words = text.Split(' ');
34+
foreach (string word in words)
35+
{
36+
streamingRender.Refresh(word + " ");
37+
}
38+
}
39+
else
40+
{
41+
host.RenderFullResponse(text);
42+
}
2843
}
2944
catch (Exception e)
3045
{

0 commit comments

Comments
 (0)