济南住建局官方网站中国搜索引擎大全
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 OffsetChangeMapRecalculateOffsets(e);}else{// recompute all the line ranges// based in the document linesRecomputeAllLineRanges(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 tooSetInvalidRange(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 rangeif (_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());}}elsesnippet = 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 = "," });elsesnippet.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的使用等等等,东西还是挺多的给出一些实现的图片吧。
创建文件导向
创建项目导向
绘制并实现断点
编译输出
好了本文到此结束,如果你认认真真看到了这里希望你有所收获,拜拜!!!!!!!!!