如何用wpf做一个IDE?

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的使用等等等,东西还是挺多的给出一些实现的图片吧。

创建文件导向 

 

创建项目导向 

绘制并实现断点 

 

 编译输出

好了本文到此结束,如果你认认真真看到了这里希望你有所收获,拜拜!!!!!!!!! 

  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
### 回答1: WPF(Windows Presentation Foundation)是微软发布的一种创建图形用户界面的技术,它结合了XAML(可扩展应用程序标记语言)和各种.NET语言(如VB.NET)来构建交互式应用程序。如果您想学习WPF,以下是一些入门教程: 1. 官方文档:微软提供了一份详细的WPF文档,其中包括了从基础入门到高级特性的介绍,这将是您开始学习WPF的最佳选择。 2. MSDN教程:在MSDN社区中,有大量的WPF教程和文章,涵盖了从XAML基础到如何使用数据绑定和动画等高级功能的内容。 3. 视频教程:还有许多视频教程可以帮助您学习WPF,这些视频教程可以找到许多网站,比如Pluralsight和Lynda等。 4. 书籍:如果您更喜欢看书学习,这里有几本非常不错的WPF入门书籍,如“WPF入门指南”,“Visual Basic 2015 Unleashed”等。 5. 示例代码:最后,一些示例代码可能会对您的学习有所帮助,您可以使用微软提供的Visual Studio IDE(集成开发环境)来开发WPF应用程序,同时它还提供了许多WPF应用程序模板。 综上所述,这些都是WPF入门教程的主要来源,以及一些有用的资源,如果您想掌握WPF技术,最好的方法是自己动手,尝试编写WPF应用程序,多看文档和教程,并实践一些示例代码。 ### 回答2: WPF是一个新型的Windows桌面应用程序开发平台,它提供了一种基于XAML的声明式UI编程模型,使得开发者可以轻松地创建强大、灵活和现代化的用户界面。对于初学者而言,学习WPF需要掌握一些基本的概念,如XAML的语法、WPF的控件、布局、样式和绑定等等。 VB是一种流行的面向对象的编程语言,也是WPF的一种开发语言。在学习WPF时,掌握VB语言及基本的编程概念是必不可少的。要开始学习WPF,建议首先了解如何创建WPF应用程序以及如何使用Visual Studio来编写代码。 在WPF中,控件是应用程序的基本结构单元。可以使用各种控件来构建用户界面,包括按钮、标签、输入框、列表框、网格和文本框等等。控件可以使用样式来自定义其外观和样式,并使用绑定来实现数据绑定和交互。 在WPF中,还有一些高级的概念和功能,如路由事件、命令、动画和绘图等等,这些都可以让你创建出更加复杂和丰富的应用程序。建议在学习初期,专注于掌握基本的概念和语法,逐步深入学习和了解更多的高级功能,并使用实际的项目来练习和掌握WPF的技能和知识。 ### 回答3: WPF(Windows Presentation Foundation)是一个基于.NET框架的图形用户界面开发工具,它提供了一种新的方式来创建用户界面,使开发人员可以快速地创建富客户端应用程序。WPF入门教程适合有一定VB.NET编程基础的开发人员,以下是一些具体的学习建议: 第一步,建立基本的WPF应用程序框架。学习如何创建WPF应用程序,如何设置不同的前景、背景色、各种控件的大小和位置等。可以试着创建一些简单的窗口控件,并在其中添加一些基本的控件,比如文本框、标签等。 第二步,学习WPF控件。WPF提供了许多控件,如文本框、标签、按钮、列表框等等。学习每个控件的属性和事件,了解每个控件的特点和使用场景。 第三步,学习WPF布局。WPF提供了多种方式来布局控件,如栅格布局、StackPanel布局、WrapPanel布局等等。学习每种布局方式的特点和使用场景,如何设置控件在布局中的位置和大小等。 第四步,学习WPF绑定。WPF提供了一种数据绑定机制,可以将控件与数据源进行绑定,使数据源发生变化时,控件能够自动更新。学习绑定的基本语法和使用方法,并了解如何将不同类型的数据绑定到控件上。 第五步,学习WPF模板。WPF提供了一种模板机制,可以自定义控件的样式和布局。学习如何创建和使用控件模板,让控件的外观和功能更加灵活。 总之,学习WPF需要耐心和实践,多一些练习和实践项目,逐渐提升自己的技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值