1.大致的成果样子
正如封面最终的成果是这样的如图1-1,如果你想实现类似的功能这篇文章或许对你是有一点点帮助的,因为我制作该软件的时候发现国内类似的资料真的是非常少的。
图1-1 大致成果
2 主要功能介绍
本次主要用到的是WPF,最终大概实现了创建解决方案,创建项目,创建特定的文件,智能提示,语法纠错,编译等等功能。
2.1 LSP的使用
知道的小伙伴肯定知道这里说的LSP其实是language-server-protocol。并不是有些小伙伴想的那种LSP😍,lsp的东西实在是太多了我这里就直接给出文档链接了Specification (microsoft.github.io)
简而言之就是如果我们启动了某一个LSP的服务端,我们只要在我们的wpf程序中实现一个对应的客户端即可,只要实现了这个客户端理论上我们就可以让我们的程序像vscode一样安装一个插件就能实现某一种语言的智能提示,codelens,语法纠错等等等你在visual studio中所看到的ide所必备的功能,就是怎么神奇。
2.2 认识Clangd
如果你认识Clangd你就可以看下一个内容了,如果不太了解我就大致的说一下这是个什么玩意,英文我这次实现的是C语言的智能提示,编译等等功能,其他语言我可能考虑后期扩展。
Clangd是一个基于LLVM的开源C/C++语言服务器,用于提供C/C++代码的语义分析和编辑功能。它是Clang工具集的一部分,Clang是一个强大的C/C++编译器前端。
Clangd提供了一种与编辑器交互的方式,使编辑器能够实时分析和理解代码的语义结构。它可以用于代码补全、自动重构、错误和警告提示、跳转到定义/声明、查找引用等功能。Clangd利用了Clang编译器的强大功能,能够进行复杂的语义分析,包括类型推断、语法检查、静态分析等。
Clangd可以与多个主流编辑器集成,如Visual Studio Code、Atom、Sublime Text等。它通过与编辑器通信,提供有关代码的实时反馈和智能建议,提高了开发者的工作效率和代码质量。
总结来说,Clangd是一个用于C/C++代码的语义分析和编辑的语言服务器,提供了丰富的编辑器集成功能,帮助开发者更好地理解和编写代码。
2.3 OminSharp是什么
OmniSharp是一个C#开源的库你可以用OmniSharp快速的实现一个lsp服务器,或者lsp客户端,毫无疑问我们要做的就是客户端啦,我们就是要用OmniSharp来对接clangd来实现这个ide的绝大部分的功能,当然这个lsp也不是万能的,比如对Snippet支持好像不是很好,(Snippet其实就是语法块啦,就像我们在vs中按下propfull就会出现如下:
private int myVar;
public int MyProperty
{
get { return myVar; }
set { myVar = value; }
}
就类似这样的功能,clangd好像是无法完成的(如果可以实现欢迎大佬留言批评指正)。当然这个功能我会在下面讲解。OminSharp的简单使用我会在后面贴出代码。
2.4 AvalonEdit
我相信搞wpf的大多数应该都认识这个控件吧,这个控件有很多好用的功能,你可以直接在nuget上找到他。总而言之这是一个非常非常强大的编辑器,他提供了行号呀,Snippet呀,高亮支持呀等等好用的功能,我相信你要用WPF做一个ide大概率是避不开他了,可以好好看一下这个开源项目。但我这里也有一些其他东西要说的。
我们知道AvalonEdit可以通过配置xshd文件来实现语法的高亮。我这边用了另一套解决方案,那就是用TextMate来实现高亮,因为我觉得这个高亮真的很好看,灵感来自这里danipen/TextMateSharp:tm4e的移植,将TextMate语法引入dotnet生态系统 (github.com)
但是这个库仅仅支持 AvaloniaEdit
我们可以对这个库进行一点点小的改装让他支持AvalonEdit,改造后的代码如下:
public class DocumentSnapshot
{
private LineRange[] _lineRanges;
private TextDocument _document;
private ITextSource _textSource;
private object _lock = new object();
private int _lineCount;
public int LineCount
{
get { lock (_lock) { return _lineCount; } }
}
public DocumentSnapshot(TextDocument document)
{
_document = document;
_lineRanges = new LineRange[document.LineCount];
ICSharpCode.AvalonEdit.TextEditor textEditor = new ICSharpCode.AvalonEdit.TextEditor();
Update(null);
}
public void RemoveLines(int startLine, int endLine)
{
lock (_lock)
{
var tmpList = _lineRanges.ToList();
tmpList.RemoveRange(startLine, endLine - startLine + 1);
_lineRanges = tmpList.ToArray();
_lineCount = _lineRanges.Length;
}
}
public string GetLineText(int lineIndex)
{
lock (_lock)
{
var lineRange = _lineRanges[lineIndex];
return _textSource.GetText(lineRange.Offset, lineRange.Length);
}
}
public string GetLineTextIncludingTerminator(int lineIndex)
{
lock (_lock)
{
var lineRange = _lineRanges[lineIndex];
return _textSource.GetText(lineRange.Offset, lineRange.TotalLength);
}
}
public string GetLineTerminator(int lineIndex)
{
lock (_lock)
{
var lineRange = _lineRanges[lineIndex];
return _textSource.GetText(lineRange.Offset + lineRange.Length, lineRange.TotalLength - lineRange.Length);
}
}
public int GetLineLength(int lineIndex)
{
lock (_lock)
{
return _lineRanges[lineIndex].Length;
}
}
public int GetTotalLineLength(int lineIndex)
{
lock (_lock)
{
return _lineRanges[lineIndex].TotalLength;
}
}
public string GetText()
{
lock (_lock)
{
return _textSource.Text;
}
}
public void Update(DocumentChangeEventArgs e)
{
lock (_lock)
{
_lineCount = _document.Lines.Count;
if (e != null && e.OffsetChangeMap != null && _lineRanges != null && _lineCount == _lineRanges.Length)
{
// it's a single-line change
// update the offsets usign the OffsetChangeMap
RecalculateOffsets(e);
}
else
{
// recompute all the line ranges
// based in the document lines
RecomputeAllLineRanges(e);
}
_textSource = _document.CreateSnapshot();
}
}
private void RecalculateOffsets(DocumentChangeEventArgs e)
{
var changedLine = _document.GetLineByOffset(e.Offset);
int lineIndex = changedLine.LineNumber - 1;
_lineRanges[lineIndex].Offset = changedLine.Offset;
_lineRanges[lineIndex].Length = changedLine.Length;
_lineRanges[lineIndex].TotalLength = changedLine.TotalLength;
for (int i = lineIndex + 1; i < _lineCount; i++)
{
_lineRanges[i].Offset = e.OffsetChangeMap.GetNewOffset(_lineRanges[i].Offset);
}
}
private void RecomputeAllLineRanges(DocumentChangeEventArgs e)
{
Array.Resize(ref _lineRanges, _lineCount);
int currentLineIndex = (e != null) ?
_document.GetLineByOffset(e.Offset).LineNumber - 1 : 0;
var currentLine = _document.GetLineByNumber(currentLineIndex + 1);
while (currentLine != null)
{
_lineRanges[currentLineIndex].Offset = currentLine.Offset;
_lineRanges[currentLineIndex].Length = currentLine.Length;
_lineRanges[currentLineIndex].TotalLength = currentLine.TotalLength;
currentLine = currentLine.NextLine;
currentLineIndex++;
}
}
struct LineRange
{
public int Offset;
public int Length;
public int TotalLength;
}
}
public abstract class GenericLineTransformer : DocumentColorizingTransformer
{
private Action<Exception> _exceptionHandler;
public GenericLineTransformer(Action<Exception> exceptionHandler)
{
_exceptionHandler = exceptionHandler;
}
protected override void ColorizeLine(DocumentLine line)
{
try
{
TransformLine(line, CurrentContext);
}
catch (Exception ex)
{
_exceptionHandler?.Invoke(ex);
}
}
protected abstract void TransformLine(DocumentLine line, ITextRunConstructionContext context);
public void SetTextStyle(
DocumentLine line,
int startIndex,
int length,
Brush foreground,
Brush background,
FontStyle fontStyle,
FontWeight fontWeigth,
bool isUnderline)
{
int startOffset = 0;
int endOffset = 0;
if (startIndex >= 0 && length > 0)
{
if ((line.Offset + startIndex + length) > line.EndOffset)
{
length = (line.EndOffset - startIndex) - line.Offset - startIndex;
}
startOffset = line.Offset + startIndex;
endOffset = line.Offset + startIndex + length;
}
else
{
startOffset = line.Offset;
endOffset = line.EndOffset;
}
if (startOffset > CurrentContext.Document.TextLength ||
endOffset > CurrentContext.Document.TextLength)
return;
ChangeLinePart(
startOffset,
endOffset,
visualLine => ChangeVisualLine(visualLine, foreground, background, fontStyle, fontWeigth, isUnderline));
}
void ChangeVisualLine(
VisualLineElement visualLine,
Brush foreground,
Brush background,
FontStyle fontStyle,
FontWeight fontWeigth,
bool isUnderline)
{
if (foreground != null)
visualLine.TextRunProperties.SetForegroundBrush(foreground);
if (background != null)
visualLine.TextRunProperties.SetBackgroundBrush(background);
if (isUnderline)
{
visualLine.TextRunProperties.SetTextDecorations(TextDecorations.Underline);
}
if (visualLine.TextRunProperties.Typeface.Style != fontStyle ||
visualLine.TextRunProperties.Typeface.Weight != fontWeigth)
{
visualLine.TextRunProperties.SetTypeface(new Typeface(
visualLine.TextRunProperties.Typeface.FontFamily, fontStyle, fontWeigth, new FontStretch()));
}
}
}
public class TextEditorModel : AbstractLineList, IDisposable
{
private readonly TextDocument _document;
private readonly TextView _textView;
private DocumentSnapshot _documentSnapshot;
private Action<Exception> _exceptionHandler;
private InvalidLineRange _invalidRange;
public DocumentSnapshot DocumentSnapshot { get { return _documentSnapshot; } }
internal InvalidLineRange InvalidRange { get { return _invalidRange; } }
public TextEditorModel(TextView textView, TextDocument document, Action<Exception> exceptionHandler)
{
_textView = textView;
_document = document;
_exceptionHandler = exceptionHandler;
_documentSnapshot = new DocumentSnapshot(_document);
for (int i = 0; i < _document.LineCount; i++)
AddLine(i);
_document.Changing += DocumentOnChanging;
_document.Changed += DocumentOnChanged;
_document.UpdateFinished += DocumentOnUpdateFinished;
_textView.ScrollOffsetChanged += TextView_ScrollOffsetChanged;
}
public override void Dispose()
{
_document.Changing -= DocumentOnChanging;
_document.Changed -= DocumentOnChanged;
_document.UpdateFinished -= DocumentOnUpdateFinished;
_textView.ScrollOffsetChanged -= TextView_ScrollOffsetChanged;
}
public override void UpdateLine(int lineIndex) { }
public void InvalidateViewPortLines()
{
if (!_textView.VisualLinesValid ||
_textView.VisualLines.Count == 0)
return;
InvalidateLineRange(
_textView.VisualLines[0].FirstDocumentLine.LineNumber - 1,
_textView.VisualLines[_textView.VisualLines.Count - 1].LastDocumentLine.LineNumber - 1);
}
public override int GetNumberOfLines()
{
return _documentSnapshot.LineCount;
}
public override string GetLineText(int lineIndex)
{
return _documentSnapshot.GetLineText(lineIndex);
}
public override int GetLineLength(int lineIndex)
{
return _documentSnapshot.GetLineLength(lineIndex);
}
private void TextView_ScrollOffsetChanged(object sender, EventArgs e)
{
try
{
TokenizeViewPort();
}
catch (Exception ex)
{
_exceptionHandler?.Invoke(ex);
}
}
private void DocumentOnChanging(object sender, DocumentChangeEventArgs e)
{
try
{
if (e.RemovalLength > 0)
{
var startLine = _document.GetLineByOffset(e.Offset).LineNumber - 1;
var endLine = _document.GetLineByOffset(e.Offset + e.RemovalLength).LineNumber - 1;
for (int i = endLine; i > startLine; i--)
{
RemoveLine(i);
}
_documentSnapshot.RemoveLines(startLine, endLine);
}
}
catch (Exception ex)
{
_exceptionHandler?.Invoke(ex);
}
}
private void DocumentOnChanged(object sender, DocumentChangeEventArgs e)
{
try
{
int startLine = _document.GetLineByOffset(e.Offset).LineNumber - 1;
int endLine = startLine;
if (e.InsertionLength > 0)
{
endLine = _document.GetLineByOffset(e.Offset + e.InsertionLength).LineNumber - 1;
for (int i = startLine; i < endLine; i++)
{
AddLine(i);
}
}
_documentSnapshot.Update(e);
if (startLine == 0)
{
SetInvalidRange(startLine, endLine);
return;
}
// some grammars (JSON, csharp, ...)
// need to invalidate the previous line too
SetInvalidRange(startLine - 1, endLine);
}
catch (Exception ex)
{
_exceptionHandler?.Invoke(ex);
}
}
private void SetInvalidRange(int startLine, int endLine)
{
if (!_document.IsInUpdate)
{
InvalidateLineRange(startLine, endLine);
return;
}
// we're in a document change, store the max invalid range
if (_invalidRange == null)
{
_invalidRange = new InvalidLineRange(startLine, endLine);
return;
}
_invalidRange.SetInvalidRange(startLine, endLine);
}
void DocumentOnUpdateFinished(object sender, EventArgs e)
{
if (_invalidRange == null)
return;
try
{
InvalidateLineRange(_invalidRange.StartLine, _invalidRange.EndLine);
}
finally
{
_invalidRange = null;
}
}
private void TokenizeViewPort()
{
Application.Current.Dispatcher.InvokeAsync(() =>
{
if (!_textView.VisualLinesValid ||
_textView.VisualLines.Count == 0)
return;
ForceTokenization(
_textView.VisualLines[0].FirstDocumentLine.LineNumber - 1,
_textView.VisualLines[_textView.VisualLines.Count - 1].LastDocumentLine.LineNumber - 1);
}, DispatcherPriority.Normal);
}
internal class InvalidLineRange
{
internal int StartLine { get; private set; }
internal int EndLine { get; private set; }
internal InvalidLineRange(int startLine, int endLine)
{
StartLine = startLine;
EndLine = endLine;
}
internal void SetInvalidRange(int startLine, int endLine)
{
if (startLine < StartLine)
StartLine = startLine;
if (endLine > EndLine)
EndLine = endLine;
}
}
}
public static class TextMate
{
public static void RegisterExceptionHandler(Action<Exception> handler)
{
_exceptionHandler = handler;
}
public static Installation InstallTextMate(
this TextEditor editor,
IRegistryOptions registryOptions,
bool initCurrentDocument = true)
{
return new Installation(editor, registryOptions, initCurrentDocument);
}
public class Installation
{
private IRegistryOptions _textMateRegistryOptions;
private Registry _textMateRegistry;
private TextEditor _editor;
private TextEditorModel _editorModel;
private IGrammar _grammar;
private TMModel _tmModel;
private TextMateColoringTransformer _transformer;
public IRegistryOptions RegistryOptions { get { return _textMateRegistryOptions; } }
public TextEditorModel EditorModel { get { return _editorModel; } }
public Installation(TextEditor editor, IRegistryOptions registryOptions, bool initCurrentDocument = true)
{
_textMateRegistryOptions = registryOptions;
_textMateRegistry = new Registry(registryOptions);
_editor = editor;
SetTheme(registryOptions.GetDefaultTheme());
editor.DocumentChanged += OnEditorOnDocumentChanged;
if (initCurrentDocument)
{
OnEditorOnDocumentChanged(editor, EventArgs.Empty);
}
}
public void SetGrammar(string scopeName)
{
_grammar = _textMateRegistry.LoadGrammar(scopeName);
GetOrCreateTransformer().SetGrammar(_grammar);
_editor.TextArea.TextView.Redraw();
}
public void SetTheme(IRawTheme theme)
{
_textMateRegistry.SetTheme(theme);
GetOrCreateTransformer().SetTheme(_textMateRegistry.GetTheme());
_tmModel?.InvalidateLine(0);
_editorModel?.InvalidateViewPortLines();
}
public void Dispose()
{
_editor.DocumentChanged -= OnEditorOnDocumentChanged;
DisposeEditorModel(_editorModel);
DisposeTMModel(_tmModel, _transformer);
DisposeTransformer(_transformer);
}
void OnEditorOnDocumentChanged(object sender, EventArgs args)
{
try
{
DisposeEditorModel(_editorModel);
DisposeTMModel(_tmModel, _transformer);
_editorModel = new TextEditorModel(_editor.TextArea.TextView, _editor.Document, _exceptionHandler);
_tmModel = new TMModel(_editorModel);
_tmModel.SetGrammar(_grammar);
_transformer = GetOrCreateTransformer();
_transformer.SetModel(_editor.Document, _tmModel);
_tmModel.AddModelTokensChangedListener(_transformer);
}
catch (Exception ex)
{
_exceptionHandler?.Invoke(ex);
}
}
TextMateColoringTransformer GetOrCreateTransformer()
{
var transformer = _editor.TextArea.TextView.LineTransformers.OfType<TextMateColoringTransformer>().FirstOrDefault();
if (transformer is null)
{
transformer = new TextMateColoringTransformer(
_editor.TextArea.TextView, _exceptionHandler);
_editor.TextArea.TextView.LineTransformers.Add(transformer);
}
return transformer;
}
static void DisposeTransformer(TextMateColoringTransformer transformer)
{
if (transformer == null)
return;
transformer.Dispose();
}
static void DisposeTMModel(TMModel tmModel, TextMateColoringTransformer transformer)
{
if (tmModel == null)
return;
if (transformer != null)
tmModel.RemoveModelTokensChangedListener(transformer);
tmModel.Dispose();
}
static void DisposeEditorModel(TextEditorModel editorModel)
{
if (editorModel == null)
return;
editorModel.Dispose();
}
}
static Action<Exception> _exceptionHandler;
}
public class TextMateColoringTransformer :
GenericLineTransformer,
IModelTokensChangedListener,
ForegroundTextTransformation.IColorMap
{
private Theme _theme;
private IGrammar _grammar;
private TMModel _model;
private TextDocument _document;
private TextView _textView;
private Action<Exception> _exceptionHandler;
private volatile bool _areVisualLinesValid = false;
private volatile int _firstVisibleLineIndex = -1;
private volatile int _lastVisibleLineIndex = -1;
private readonly Dictionary<int, Brush> _brushes;
public TextMateColoringTransformer(
TextView textView,
Action<Exception> exceptionHandler)
: base(exceptionHandler)
{
_textView = textView;
_exceptionHandler = exceptionHandler;
_brushes = new Dictionary<int, Brush>();
_textView.VisualLinesChanged += TextView_VisualLinesChanged;
}
public void SetModel(TextDocument document, TMModel model)
{
_areVisualLinesValid = false;
_document = document;
_model = model;
if (_grammar != null)
{
_model.SetGrammar(_grammar);
}
}
private void TextView_VisualLinesChanged(object sender, EventArgs e)
{
try
{
if (!_textView.VisualLinesValid || _textView.VisualLines.Count == 0)
return;
_areVisualLinesValid = true;
_firstVisibleLineIndex = _textView.VisualLines[0].FirstDocumentLine.LineNumber - 1;
_lastVisibleLineIndex = _textView.VisualLines[_textView.VisualLines.Count - 1].LastDocumentLine.LineNumber - 1;
}
catch (Exception ex)
{
_exceptionHandler?.Invoke(ex);
}
}
public void Dispose()
{
_textView.VisualLinesChanged -= TextView_VisualLinesChanged;
}
public void SetTheme(Theme theme)
{
_theme = theme;
_brushes.Clear();
var map = _theme.GetColorMap();
foreach (var color in map)
{
var id = _theme.GetColorId(color);
Color c = (Color)ColorConverter.ConvertFromString(color);
_brushes[id] = new SolidColorBrush(c);
}
}
public void SetGrammar(IGrammar grammar)
{
_grammar = grammar;
if (_model != null)
{
_model.SetGrammar(grammar);
}
}
Brush ForegroundTextTransformation.IColorMap.GetBrush(int colorId)
{
if (_brushes == null)
return null;
_brushes.TryGetValue(colorId, out Brush result);
return result;
}
protected override void TransformLine(DocumentLine line, ITextRunConstructionContext context)
{
try
{
if (_model == null)
return;
int lineNumber = line.LineNumber;
var tokens = _model.GetLineTokens(lineNumber - 1);
if (tokens == null)
return;
var transformsInLine = ArrayPool<ForegroundTextTransformation>.Shared.Rent(tokens.Count);
try
{
GetLineTransformations(lineNumber, tokens, transformsInLine);
for (int i = 0; i < tokens.Count; i++)
{
if (transformsInLine[i] == null)
continue;
transformsInLine[i].Transform(this, line);
}
}
finally
{
ArrayPool<ForegroundTextTransformation>.Shared.Return(transformsInLine);
}
}
catch (Exception ex)
{
_exceptionHandler?.Invoke(ex);
}
}
private void GetLineTransformations(int lineNumber, List<TMToken> tokens, ForegroundTextTransformation[] transformations)
{
for (int i = 0; i < tokens.Count; i++)
{
var token = tokens[i];
var nextToken = (i + 1) < tokens.Count ? tokens[i + 1] : null;
var startIndex = token.StartIndex;
var endIndex = nextToken?.StartIndex ?? _model.GetLines().GetLineLength(lineNumber - 1);
if (startIndex >= endIndex || token.Scopes == null || token.Scopes.Count == 0)
{
transformations[i] = null;
continue;
}
var lineOffset = _document.GetLineByNumber(lineNumber).Offset;
int foreground = 0;
int background = 0;
int fontStyle = 0;
foreach (var themeRule in _theme.Match(token.Scopes))
{
if (foreground == 0 && themeRule.foreground > 0)
foreground = themeRule.foreground;
if (background == 0 && themeRule.background > 0)
background = themeRule.background;
if (fontStyle == 0 && themeRule.fontStyle > 0)
fontStyle = themeRule.fontStyle;
}
if (transformations[i] == null)
transformations[i] = new ForegroundTextTransformation();
transformations[i].ColorMap = this;
transformations[i].ExceptionHandler = _exceptionHandler;
transformations[i].StartOffset = lineOffset + startIndex;
transformations[i].EndOffset = lineOffset + endIndex;
transformations[i].ForegroundColor = foreground;
transformations[i].BackgroundColor = background;
transformations[i].FontStyle = fontStyle;
}
}
public void ModelTokensChanged(ModelTokensChangedEvent e)
{
if (e.Ranges == null)
return;
if (_model == null || _model.IsStopped)
return;
int firstChangedLineIndex = int.MaxValue;
int lastChangedLineIndex = -1;
foreach (var range in e.Ranges)
{
firstChangedLineIndex = Math.Min(range.FromLineNumber - 1, firstChangedLineIndex);
lastChangedLineIndex = Math.Max(range.ToLineNumber - 1, lastChangedLineIndex);
}
if (_areVisualLinesValid)
{
bool changedLinesAreNotVisible =
((firstChangedLineIndex < _firstVisibleLineIndex && lastChangedLineIndex < _firstVisibleLineIndex) ||
(firstChangedLineIndex > _lastVisibleLineIndex && lastChangedLineIndex > _lastVisibleLineIndex));
if (changedLinesAreNotVisible)
return;
}
Application.Current.Dispatcher.Invoke(() =>
{
int firstLineIndexToRedraw = Math.Max(firstChangedLineIndex, _firstVisibleLineIndex);
int lastLineIndexToRedrawLine = Math.Min(lastChangedLineIndex, _lastVisibleLineIndex);
int totalLines = _document.Lines.Count - 1;
firstLineIndexToRedraw = Clamp(firstLineIndexToRedraw, 0, totalLines);
lastLineIndexToRedrawLine = Clamp(lastLineIndexToRedrawLine, 0, totalLines);
DocumentLine firstLineToRedraw = _document.Lines[firstLineIndexToRedraw];
DocumentLine lastLineToRedraw = _document.Lines[lastLineIndexToRedrawLine];
_textView.Redraw(
firstLineToRedraw.Offset,
(lastLineToRedraw.Offset + lastLineToRedraw.TotalLength) - firstLineToRedraw.Offset);
});
}
static int Clamp(int value, int min, int max)
{
if (value < min)
return min;
if (value > max)
return max;
return value;
}
static string NormalizeColor(string color)
{
if (color.Length == 9)
{
Span<char> normalizedColor = stackalloc char[] { '#', color[7], color[8], color[1], color[2], color[3], color[4], color[5], color[6] };
return normalizedColor.ToString();
}
return color;
}
}
public abstract class TextTransformation : TextSegment
{
public abstract void Transform(GenericLineTransformer transformer, DocumentLine line);
}
public class ForegroundTextTransformation : TextTransformation
{
public interface IColorMap
{
Brush GetBrush(int color);
}
public IColorMap ColorMap { get; set; }
public Action<Exception> ExceptionHandler { get; set; }
public int ForegroundColor { get; set; }
public int BackgroundColor { get; set; }
public int FontStyle { get; set; }
public override void Transform(GenericLineTransformer transformer, DocumentLine line)
{
try
{
if (Length == 0)
{
return;
}
var formattedOffset = 0;
var endOffset = line.EndOffset;
if (StartOffset > line.Offset)
{
formattedOffset = StartOffset - line.Offset;
}
if (EndOffset < line.EndOffset)
{
endOffset = EndOffset;
}
transformer.SetTextStyle(line, formattedOffset, endOffset - line.Offset - formattedOffset,
ColorMap.GetBrush(ForegroundColor),
ColorMap.GetBrush(BackgroundColor),
GetFontStyle(),
GetFontWeight(),
IsUnderline());
}
catch (Exception ex)
{
ExceptionHandler?.Invoke(ex);
}
}
FontStyle GetFontStyle()
{
FontStyle style = new FontStyle();
if (FontStyle != TextMateSharp.Themes.FontStyle.NotSet &&
(FontStyle & TextMateSharp.Themes.FontStyle.Italic) != 0)
return FontStyles.Italic;
return FontStyles.Normal;
}
FontWeight GetFontWeight()
{
if (FontStyle != TextMateSharp.Themes.FontStyle.NotSet &&
(FontStyle & TextMateSharp.Themes.FontStyle.Bold) != 0)
return FontWeights.Bold;
return FontWeights.Regular;
}
bool IsUnderline()
{
if (FontStyle != TextMateSharp.Themes.FontStyle.NotSet &&
(FontStyle & TextMateSharp.Themes.FontStyle.Underline) != 0)
return true;
return false;
}
}
代码挺多的不想看也可以直接拿来用,下面我给大家看看是个什么效果,我是将高亮和我的主题绑定到一起的
图2-4-1 高亮1
图2-4-2 高亮2
图2-4-2 高亮3
我这里就简单的展示3个你可以根据自己的需要来 ,怎么使用的我这里就不做详细介绍,你可以看我贴出的源码,或者去AvaloniaUI/AvaloniaEdit: Avalonia-based text editor (port of AvalonEdit) (github.com)
这里看看是怎么用的 一定要记住用我上面贴出的源码。
3 AvalonEdit+OmniSharp+Clangd的联合使用
AvalonEdit和OmniSharp都可在Nuget上下载到,Clangd是一个exe可执行文件我们需要到官网上去下载地址在这里Release 16.0.2 · clangd/clangd · GitHub
下载后你会得到一个exe文件
图3-1 clangd
3.1在C#中用 OmniSharp链接Clangd
我们知道 Clangd就类似服务端,我们的OmniSharp就类似客户端,现在我们需要将两端连接起来,建立通讯,我写的代码如下你可以作为参考:
[Export(typeof(ICppLsp))]
public class CppLSP:ICppLsp
{
public readonly string clangd = @$"{AppDomain.CurrentDomain.BaseDirectory}libs\clangd-windows-15.0.6\clangd_15.0.6\bin\clangd.exe";
private Process? _lspServer;
private LanguageClient? _client;
private string root;
public LanguageClient LanguageClient => _client;
public event FileSystemEventHandler OnFileChanged;
public event EventHandler<PublishDiagnosticsParams> OnPublishDiagnostics;
public event EventHandler<CodeLensRefreshParams> OnCodeLensRefresh;
System.IO.FileSystemWatcher watcher;
public async Task StartAsync(string root)
{
try
{
_lspServer?.Close();
_lspServer?.Dispose();
_lspServer = null;
this.root = root;
watcher = new System.IO.FileSystemWatcher(root);
if (_lspServer is null)
{
var startInfo = new ProcessStartInfo(clangd,
@" --log=verbose --all-scopes-completion --completion-style=detailed --header-insertion=never -background-index");
startInfo.RedirectStandardInput = true;
startInfo.RedirectStandardOutput = true;
startInfo.CreateNoWindow = true;
_lspServer = Process.Start(startInfo);
LanguageServerOptions languageServerOptions = new LanguageServerOptions();
}
if ((_lspServer is not null) && (_client is null))
{
var options = new LanguageClientOptions();
options.Input = PipeReader.Create(_lspServer.StandardOutput.BaseStream);
options.Output = PipeWriter.Create(_lspServer.StandardInput.BaseStream);
options.RootUri = DocumentUri.From(root);
options.WithCapability(
new DocumentSymbolCapability
{
DynamicRegistration = true,
SymbolKind = new SymbolKindCapabilityOptions
{
ValueSet = new Container<SymbolKind>(
Enum.GetValues(typeof(SymbolKind)).Cast<SymbolKind>()
.ToArray()
)
},
TagSupport = new TagSupportCapabilityOptions
{
ValueSet = new[] { SymbolTag.Deprecated }
},
HierarchicalDocumentSymbolSupport = true,
LabelSupport = true,
}
);
options.OnPublishDiagnostics((options) =>
{
this.OnPublishDiagnostics?.Invoke(this, options);
});
options.OnCodeLensRefresh((hander) =>
{
OnCodeLensRefresh?.Invoke(this, hander);
});
_client = LanguageClient.Create(options);
var token = new CancellationToken();
await _client.Initialize(token);
}
}
catch (Exception ex)
{
}
watcher.EnableRaisingEvents = true;
watcher.IncludeSubdirectories = true;
watcher.Changed += Watcher_Changed;
}
private void Watcher_Changed(object sender, FileSystemEventArgs e)
{
OnFileChanged?.Invoke(sender, e);
}
}
这样我们就连接上了Clangd,当然我这里是用Clangd做示例,你可以连接任何其他的LSP服务,我们得到了一个LanguageClient,和发布诊断的事件,这个事件会在我们有语法错误或者警告的时候触发,触发后我们就可以绘制波浪线提示用户,接下我们就做一个简单的实验用这个LanguageClient来实现鼠标悬浮提示我先给大家看一下效果:
图 3-1-1 鼠标悬浮效果
如图3-1-1我们将鼠标悬浮到abs函数上会弹出图上的提示内容,这是怎么实现的呢,下面我给大家看一下实现代码,其实非常的简单:
private async void CodeEditor_MouseHover(object sender, System.Windows.Input.MouseEventArgs e)
{
if (LanguageClient is not null)
{
var position = GetPositionFromPoint(e.GetPosition(this));
if (position != null && position.HasValue)
{
Hover hover = await LanguageClient.RequestHover(new HoverParams
{
Position = new Position(position.Value.Location.Line - 1, position.Value.Location.Column - 1),
TextDocument = TextDocument
});
if (hover != null)
{
Application.Current.Dispatcher.Invoke(() =>
{
if (CaretOffset >= 0)
{
DocumentLine line = Document.GetLineByOffset(CaretOffset);
int lineNumber = line.LineNumber;
int columnNumber = CaretOffset - line.Offset;
string tipText = hover.ToString();
toolTip.Content = tipText;
toolTip.IsEnabled = true;
toolTip.PlacementTarget = TextArea;
Point p = e.GetPosition(TextArea);
p.Offset(10, 10);
toolTip.Placement = PlacementMode.Relative;
toolTip.HorizontalOffset = p.X;
toolTip.VerticalOffset = p.Y;
TextArea.ToolTip = toolTip;
toolTip.IsOpen = true;
}
else
{
}
});
}
}
// await CodeActionAsync(this.Diagnostics);
}
这个LanguageClient就是之前提到过的对象,重点看一下这个函数
Hover hover = await LanguageClient.RequestHover(new HoverParams
{
Position = new Position(position.Value.Location.Line - 1, position.Value.Location.Column - 1),
TextDocument = TextDocument
});
这是向clangd发送了一个hover请求,参数的含义可以去看LSP的官方文档,文档链接我在本文的2.1小节中已经给出了地址这里的Position其实就是当前鼠标的位置,TextDocument其实就是当前文档对象,比如文档的路径,文档的内容等等。该请求会返回一个Hover对象。Hover对象中有所有你想要的内容。
最后在演示一下智能提示是怎么实现的吧:
public async Task CompletionAsync(char currentCahr)
{
if (completionWindow != null) return;
if (LanguageClient is null) return;
var result = await LanguageClient.RequestCompletion(new CompletionParams
{
TextDocument = TextDocument,
Position = Position,
});
completionWindow = new CompletionWindow(TextArea);
completionWindow.Closed += (s, e) =>
{
completionWindow = null;
};
IList<ICompletionData> data = completionWindow.CompletionList.CompletionData;
//completionWindow.CompletionList.IsFiltering = true;
var completions = this.manager.GetCompletionSnippet(_registryOptions.GetLanguageByExtension(Path.GetExtension(FilePath)).Id).Where(x => x.Text.Contains(currentCahr));
foreach (var item in completions)
{
completionWindow.CompletionList.CompletionData.Add(item);
}
foreach (var item in result)
{
completionWindow.CompletionList.CompletionData.Add(new CompletionData(item));
}
if (data.Count > 0)
{
completionWindow.CompletionList.SelectedItem = data[0];
completionWindow.Background = (SolidColorBrush)Application.Current.FindResource("SystemColorsWindow");
completionWindow.Foreground = (SolidColorBrush)Application.Current.FindResource("SystemColorsWindowText");
completionWindow.BorderBrush = (SolidColorBrush)Application.Current.FindResource("EnvironmentMainWindowActiveDefaultBorder");
completionWindow.BorderThickness = new Thickness(1);
completionWindow.StartOffset -= 1;
completionWindow.Show();
completionWindow.CompletionList.ListBox.Items.CurrentChanged += (s, e) =>
{
if (completionWindow.CompletionList.ListBox.Items.Count == 0)
{
completionWindow.Close();
}
};
}
else
{
completionWindow.Close();
}
}
internal class CompletionData : PropertyChangedBase, ICompletionData
{
public readonly CompletionItem item;
private ImageSource image;
public CompletionItemKind Kind { get; private set; }
public CompletionData(CompletionItem item)
{
image = GetImageSource(item);
if (item.Kind != CompletionItemKind.Function)
{
Text = item.InsertText;
}
else
{
Text = item.Label;
}
this.item = item;
Kind = item.Kind;
}
public System.Windows.Media.ImageSource Image
{
get { return image; }
}
public string Text { get; private set; }
// Use this property if you want to show a fancy UIElement in the drop down list.
public object Content
{
get
{
return Text;
}
}
public object Description
{
get { return item; }
}
public double Priority { get { return 0; } }
public async void Complete(TextArea textArea, ISegment completionSegment, EventArgs insertionRequestEventArgs)
{
textArea.Document.Replace(completionSegment.Offset, completionSegment.Length, "");
var location = textArea.Document.GetLocation(textArea.Caret.Offset);
Snippet snippet = null;
if (item.Kind == CompletionItemKind.Snippet)
{
if (item.InsertText == "return")
{
snippet = new Snippet();
snippet.Elements.Add(new SnippetTextElement { Text = "return " });
snippet.Elements.Add(new SnippetReplaceableTextElement { Text = "result" });
snippet.Elements.Add(new SnippetTextElement { Text = "; " });
snippet.Elements.Add(new SnippetCaretElement());
}
else if (item.InsertText == "include")
{
snippet = new Snippet();
if (item.Label == " include \"header\"")
{
snippet.Elements.Add(new SnippetTextElement { Text = "include " });
snippet.Elements.Add(new SnippetTextElement { Text = "\"" });
snippet.Elements.Add(new SnippetReplaceableTextElement { Text = "header" });
snippet.Elements.Add(new SnippetTextElement { Text = "\"" });
snippet.Elements.Add(new SnippetCaretElement());
}
else
{
snippet.Elements.Add(new SnippetTextElement { Text = "include " });
snippet.Elements.Add(new SnippetTextElement { Text = "<" });
snippet.Elements.Add(new SnippetReplaceableTextElement { Text = "header" });
snippet.Elements.Add(new SnippetTextElement { Text = ">" });
snippet.Elements.Add(new SnippetCaretElement());
}
}
else
snippet = SnippetParser.Parse(completionSegment.Offset, location.Line, location.Column, item.Label);
}
else if (item.Kind == CompletionItemKind.Function)
{
string text = item.Label.TrimStart(' ');
string functionName = item.InsertText;
string pattern = $@"{functionName}\s*\((?<args>.*)\)";
Match match = Regex.Match(text, pattern);
if (match.Success)
{
string argsString = match.Groups["args"].Value;
string[] argsList = argsString.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
snippet = new Snippet();
snippet.Elements.Add(new SnippetTextElement { Text = functionName + "(" });
for (int i = 0; i < argsList.Length; i++)
{
snippet.Elements.Add(new SnippetReplaceableTextElement { Text = argsList[i] });
if (i != argsList.Length - 1)
snippet.Elements.Add(new SnippetTextElement { Text = "," });
else
snippet.Elements.Add(new SnippetTextElement { Text = ")" });
}
snippet.Elements.Add(new SnippetCaretElement());
}
}
if (snippet is null)
snippet = SnippetParser.Parse(completionSegment.Offset, location.Line, location.Column, item.Label.TrimStart(' '));
snippet.Insert(textArea);
}
private ImageSource GetImageSource(CompletionItem item)
{
BitmapImage bitmap = new BitmapImage();
bitmap.BeginInit();
string imagePath = string.Empty;
switch (item.Kind)
{
case CompletionItemKind.Text:
imagePath = "TextArea.png";
break;
case CompletionItemKind.Method:
imagePath = "MethodPublic.png";
break;
case CompletionItemKind.Function:
imagePath = "MethodPublic.png";
break;
case CompletionItemKind.Constructor:
imagePath = "ConstantPublic.png";
break;
case CompletionItemKind.Field:
imagePath = "FieldPublic.png";
break;
case CompletionItemKind.Variable:
imagePath = "VariableProperty.png";
break;
case CompletionItemKind.Class:
imagePath = "ClassPublic.png";
break;
case CompletionItemKind.Interface:
imagePath = "InterfacePublic.png";
break;
case CompletionItemKind.Module:
imagePath = "ModulePublic.png";
break;
case CompletionItemKind.Property:
imagePath = "PropertyPublic.png";
break;
case CompletionItemKind.Unit:
imagePath = "UnitePath.png";
break;
case CompletionItemKind.Value:
imagePath = "ValueTypePublic.png";
break;
case CompletionItemKind.Enum:
imagePath = "EnumerationPublic.png";
break;
case CompletionItemKind.Keyword:
imagePath = "KeywordSnippet.png";
break;
case CompletionItemKind.Snippet:
imagePath = "Snippet.png";
break;
case CompletionItemKind.Color:
imagePath = "ColorWheel.png";
break;
case CompletionItemKind.File:
imagePath = "FileSystemEditor.png";
break;
case CompletionItemKind.Reference:
imagePath = "Reference.png";
break;
case CompletionItemKind.Folder:
imagePath = "FolderClosedPurple.png";
break;
case CompletionItemKind.EnumMember:
imagePath = "EnumerationItemPublic.png";
break;
case CompletionItemKind.Constant:
imagePath = "ConstantPublic.png";
break;
case CompletionItemKind.Struct:
imagePath = "StructurePublic.png";
break;
case CompletionItemKind.Event:
imagePath = "Event.png";
break;
case CompletionItemKind.Operator:
imagePath = "OperatorPublic.png";
break;
case CompletionItemKind.TypeParameter:
imagePath = "Type.png";
break;
default:
imagePath = "TextArea.png";
break;
}
bitmap.UriSource = new Uri($"/Assets/CodeCompiler/{imagePath}", UriKind.RelativeOrAbsolute);
bitmap.EndInit();
return bitmap;
}
}
大致就是这样。这篇文章就先到这吧,写了这么多,太累了,下篇预告:编译功能,代码块功能,断点功能,自定义代码模板功能,自定义项目模板功能,以及该项目使用的控件样式库,AvalonDock的使用等等等,东西还是挺多的给出一些实现的图片吧。
创建文件导向
创建项目导向
绘制并实现断点
编译输出
好了本文到此结束,如果你认认真真看到了这里希望你有所收获,拜拜!!!!!!!!!