Coder Social home page Coder Social logo

simonbs / runestone Goto Github PK

View Code? Open in Web Editor NEW
2.6K 20.0 140.0 58.18 MB

πŸ“ Performant plain text editor for iOS with syntax highlighting, line numbers, invisible characters and much more.

License: MIT License

Swift 99.35% Shell 0.65%
ios swift tree-sitter

runestone's Introduction

πŸ‘‹ Welcome to Runestone - a performant plain text editor for iOS with code editing features

Runestone uses GitHub's Tree-sitter to parse code to a syntax tree which is used for features that require an understanding of the code in the editor, for example syntax highlighting.

Build and Test Build Documentation Build Example Project CodeQL SwiftLint Twitter Mastodon

✨ Features

  • Syntax highlighting.
  • Line numbers.
  • Highlight the selected line.
  • Show invisible characters (tabs, spaces and line breaks).
  • Insertion of character pairs, e.g. inserting the trailing quotation mark when inserting the leading.
  • Customization of colors and fonts.
  • Toggle line wrapping on and off.
  • Adjust height of lines.
  • Add a page guide.
  • Add vertical and horizontal overscroll.
  • Highlight ranges in the text view.
  • Search the text using regular expressions.
  • Automatically detects if a file is using spaces or tabs for indentation.
  • Specify line endings (CR, LF, CRLF) to use when inserting a line break.
  • Automatically detect line endings in a text.

πŸš€ Getting Started

Please refer to the Getting Started article in the documentation and the Meet Runestone series of tutorials.

πŸ“– Documentation

The documentation of all public types is available at docs.runestone.app. The documentation is generated from the Swift code using Apple's DocC documentation compiler.

🏎 Performance

Runestone was built to be fast. Its good performance is by far mostly thanks to Tree-sitter's incremental parsing and AvalonEdit's approach for managing lines in a document.

When judging the performance of Runestone, it is key to build your app in the release configuration. The optimizations applied by the compiler when using the release configuration becomes very apparent when opening large documents.

πŸ–₯ Catalyst

The project should mostly work with Catalyst on the Mac, however, it isn't fully tested and the implementation isn't considered done. The focus is currently on the iPhone and iPad.

πŸ“± Projects

The Runestone framework is used by an app of the same name. The Runestone app is a plain text editor for iPhone and iPad that uses all the features of this framework.

Runestone app icon

Download on the App Store

πŸ‘¨β€πŸ’» Contributing

Pull requests with bugfixes and new features are much appreciated. I'll be happy to review them and merge them once they're ready, as long as they contain change that fit within the vision of Runestone and provide generally useful functionality.

Clone the repository to get started working on the project. Note that Runestone depends on Tree-sitter through a submodule. This submodule must be cloned as well before Runestone can be built. Pass the --recursive option when cloning the repository to clone all submodules.

git clone --recursive [email protected]:simonbs/Runestone.git

❀️ Acknowledgments

runestone's People

Contributors

actuallytaylor avatar amay5267 avatar andrewbennet avatar eliperkins avatar finestructure avatar giacomopignoni avatar migueldeicaza avatar mmackh avatar multigreg avatar nighthawk avatar pasisalenius avatar rebornix avatar shinolr avatar simonbs avatar twodayslate avatar w1w1-m avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

runestone's Issues

[Feature Request] Open Files from iCloud Drive HIDDEN Folders

Hello @simonbs !!!!

My FR for the amazing Runestone is simple to explain: be able to open files (.css in my case) that are located inside iCloud Drive but in a hidden folder

πŸ’‘ Let me explain why this is so important to me :)

My Workflow

I use Obsidian as my texto editor for .md files on iPadOS using a keyboard and a mouse.

Based on the way that Obsidian was developed, all the internal content of Obsidian is inside a hidden folder that is located inside a Folder in iCloud Drive

On iCloud we have this structure:

iCloudDrive >> Obsidian Folder >> Main Folder >> .obsidian (hidden folder) >> snippets (hidden folder)

C6061175-3363-417B-A679-DDD3162D3DCF

The .obsidian/snippets folder has the .css files that re used by users to customize the Obsidian UI

  • I want to use Runestone to edit my .css files that are placed in a hidden folder.

βœ… This is the reason that i’m asking for this feature πŸ™


I’m aware that the App Textastic already have this Feature.

BUT I really love Runestone and I want to use it more in my daily workflow.

Moreover, I want to support your work because I use all your apps and I really like to the attention to details that you have with your apps πŸ’™


Thanks for reading this !

I wish you a fantastic launch day πŸš€

Add FR localization

Hi @simonbs,
Great work on Runestone, the app is nice πŸ’ͺ I'd be interested in setting up localization (if needed) and localizing it to French πŸ‡«πŸ‡· (my second tongue). If that's ok, I'll make a draft PR for this.
Regards

Minimum Gutter Width Option

It'd be great to have an option on gutter layout to specify a minimum width. Specifically to solve the text view "jumping" when hitting double (or more) digits. This would be especially nice if code is less than 3 digits.

repro

Crash report

Hi

I received several crash reports after using Runestone as the text editor within my app. But I'm not sure how to reproduce it. You may want to take a look.

Thanks for your work, by the way.

(Version 0.2.1)

Thread 0 - (TH_STATE_RUNNING)
0  CoreFoundation   ___exceptionPreprocess
1  libobjc.A.dylib  _objc_exception_throw
2  CoreFoundation   __CFThrowFormattedException
3  CoreFoundation   _mutateError
4  CoreFoundation   -[__NSCFString replaceCharactersInRange:withString:]
5  Surge-iOS        StringView.replaceText(in:with:) (StringView.swift:67:24)
6  Surge-iOS        TextInputView.replaceText(in:with:selectedRangeAfterUndo:undoActionName:) (TextInputView.swift:1115:45)
7  Surge-iOS        TextInputView.setMarkedText(_:selectedRange:) (TextInputView.swift:1296:9)
8  Surge-iOS        @objc TextInputView.setMarkedText(_:selectedRange:) (<compiler-generated>)
9  UIKitCore        -[UIResponder(UITextInput_Internal) _setAttributedMarkedText:selectedRange:]
10 UIKitCore        -[UIKBInputDelegateManager setAttributedMarkedText:selectedRange:]
11 UIKitCore        -[UIKeyboardImpl _setAttributedMarkedText:selectedRange:inputString:lastInputString:searchString:compareAttributes:]
12 UIKitCore        -[UIKeyboardImpl setMarkedText:selectedRange:inputString:lastInputString:searchString:candidateOffset:liveConversionSegments:highlighSegmentIndex:]
13 UIKitCore        -[UIKeyboardImpl assertIntermediateText:]
14 UIKitCore        -[UIKeyboardImpl syncKeyboardToConfiguration:]
15 UIKitCore        ___55-[UIKeyboardImpl handleKeyboardInput:executionContext:]_block_invoke_2
16 UIKitCore        -[UIKeyboardTaskEntry execute:]
17 UIKitCore        -[UIKeyboardTaskQueue continueExecutionOnMainThread]
18 Foundation       ___NSThreadPerformPerform
19 CoreFoundation   ___CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
20 CoreFoundation   ___CFRunLoopDoSource0
21 CoreFoundation   ___CFRunLoopDoSources0
22 CoreFoundation   ___CFRunLoopRun
23 CoreFoundation   _CFRunLoopRunSpecific
24 GraphicsServices _GSEventRunModal
25 UIKitCore        -[UIApplication _run]
26 UIKitCore        _UIApplicationMain
27 Surge-iOS        main (main.m:14:16)
28 dyld             start

String length conflation warning

In code scanning phase, CodeQL found an issue in IndentController.swift file line 71:

This String.utf16 length is used in a String, but it may not be equivalent.

Using a length value from an NSString in a String, or a count from a String in an NSString, may cause unexpected behavior including (in some cases) buffer overwrites. This is because certain unicode sequences are represented as one character in a String but as a sequence of multiple characters in an NSString. For example, a 'thumbs up' emoji with a skin tone modifier (πŸ‘πŸΏ) is represented as U+1F44D (πŸ‘) then the modifier U+1F3FF.

Crash Report

After upgrading to v0.2.2, I have received some new crash reports. (iOS 15 & 16 are all affected)

EXC_BREAKPOINT: 

0  Surge-iOS        Swift runtime failure: Unexpectedly found nil while unwrapping an Optional value (RedBlackTree.swift:40:138)
1  Surge-iOS        LayoutManager.caretRect(at:) (LayoutManager.swift:424:12)
2  Surge-iOS        TextInputView.caretRect(at:) (TextInputView.swift:983:30)
3  Surge-iOS        TextView.scroll(to:) (TextView.swift:1191:32)
4  Surge-iOS        TextInputView.replaceText(in:with:selectedRangeAfterUndo:undoActionName:) (TextInputView.swift:1160:19)
5  Surge-iOS        TextInputView.replace(_:withText:) (TextInputView.swift:1061:13)
6  Surge-iOS        TextInputView.paste(_:) (TextInputView.swift:645:13)
7  Surge-iOS        @objc TextInputView.copy(_:) (Surge-iOS)
8  UIKitCore        +[UIPasteboard _performAsDataOwner:block:]
9  UIKitCore        +[UIPasteboard _performAsDataOwnerForAction:responder:block:]
10 UIKitCore        -[UICalloutBar buttonPressed:]
11 UIKitCore        -[UICalloutBarButton fadeAndSendActionWithAuthenticationMessage:]
12 Foundation       ___NSFireDelayedPerform
13 CoreFoundation   ___CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
14 CoreFoundation   ___CFRunLoopDoTimer
15 CoreFoundation   ___CFRunLoopDoTimers
16 CoreFoundation   ___CFRunLoopRun
17 CoreFoundation   _CFRunLoopRunSpecific
18 GraphicsServices _GSEventRunModal
19 UIKitCore        -[UIApplication _run]
20 UIKitCore        _UIApplicationMain
21 Surge-iOS        main (main.m:14:16)
22 dyld             start

Automatic theme switches when toggling light/dark mode

Is your feature request related to a problem? Please describe.
The theme system allows specifying a single theme, which would be optimised for either light or dark mode. There doesn't yet seem a way to either set a single theme that supports both light and dark mode, or specify a theme per interface style.

Describe the solution you'd like
This could be addressed by providing multiple themes, one per UIUserInterfaceStyle, or by providing support for themes that support multiple interface styles.

Describe alternatives you've considered

  • I've tried adjusting the Color Set in my asset catalog to provide alternative colours for light and dark mode. This seems to work for a subset of colours, but notable not for the background colours. I'm also not sure if that's fully compatible with the internals of Runestone.
  • Detect this on my view controllers, and then set the new theme.

Additional context
I'm happy to provide a PR for this, but I'm not sure if this is desired and, if so, which approach would be the best fit for this framework.

Blocking UI on closing bracket deletion in medium size TS file

Describe the bug

Tokenization can freeze the UI for seconds in medium size TypeScript file.

To Reproduce

Expected behavior

Tokenization does not freeze the UI

Additional context

While this was tested against Runestone from App Store, I also did some profiling with the Example app and the blocking operations are syntax highlighting

image

Didn't dig into the code deep yet but a few ideas of how this can be improved

  • Tokenize asynchronously (in background thread if possible or limit how many lines are tokenized in each frame)
  • Early return when new line tokens are identical to the previous ones and skip following lines (not sure if this is valid with Tree Sitter)

Anyway to get the TreeSitterTree inside the textView?

Hello, I'm integrating the Runestone framework into my rule editor, and it's awesome, especially the highlight and line number stuffs!

And after integrating the syntax highlight, I try to do some analysis using the tree parsed by the tree-sitter, and I found the tree is stored in textView privately. So, is there any way to get that tree inside the textView?

Spam

Describe the bug
A clear and concise description of what the bug is.

To Reproduce
Steps to reproduce the behavior. Please specify the settings you have enabled on your text view, and provide a text snippet to reproduce the issue.

Expected behavior
A clear and concise description of what you expected to happen.

Screenshots
If applicable, add screenshots to help explain your problem.

Additional context
Add any other context about the problem here.

Type references that use `<doc:SomeType>` syntax within docblocks end up rendering `doc:SomeType` in source completion

Describe the bug

It seems like the docstrings that Xcode uses for it's own autocomplete don't match up what's in the generated DocC. Specifically, this only happens with references like <doc:TextView>, such at the one:

/// Creates state that can be passed to an instance of <doc:TextView>.

To Reproduce

  1. Within the project in Xcode, type TextViewState(
  2. Press ctrl+space

Expected behavior

Autocomplete within Xcode renders "Creates state that can be passed to an instance of TextView"

Actual behavior

Autocomplete within Xcode renders "Creates state that can be passed to an instance of doc:TextView"

Screenshots

Screen Shot 2022-05-31 at 4 48 22 PM

Additional context

I think that these references can use the `` syntax, but I'm not too familiar with the DocC implications here πŸ˜…

insertText(...) does not update caret position in TextView

Describe the bug
Inserting characters to a TextView programmatically, for example from a keyboard input view with quick access special keys, seems to be done by calling insertText(_ text: String) method on TextView. This adds the characters and they appear, but the caret is not moved after the newly inserted characters but stays where it was, even though the implementation seems to attempt to move the caret.

Typing more characters using the native keyboard does move the caret after the new text. Also moving the caret using arrow keys does begin moving from the position where the caret was supposed to be. So it seems like the caret position (selectedRange?) has the correct value, but it is not reflected in the TextView after text insertion.

I also tried to move the caret programmatically, by adjusting selectedRange's location property, but it results in the caret being moved too many steps ahead.

To Reproduce

  • Call textView.insertText("a") on an existing TextView instance.
  • New characters appear but the caret stays in the same location before the inserted characters.

Expected behavior
Caret should move to a position after the newly inserted characters.

API: convert offset/location to TextLocation and vice versa

Is your feature request related to a problem? Please describe.

TextView currently supports converting offset (Int) to line/column (TextLocation). With this API we could build our own Line/Column status, but if we have a Line/Column there is no API to help convert it to offset.

Describe the solution you'd like

We can have an override of textLocation API so embedders can covert between offset and line/column with the same method.

    public func textLocation(at location: Int) -> TextLocation? {
        if let linePosition = textInputView.linePosition(at: location) {
            return TextLocation(linePosition)
        } else {
            return nil
        }
    }
    
+    public func textLocation(at textLocation: TextLocation) -> Int? {
+        let lineIndex = textLocation.lineNumber
+        
+        guard lineIndex >= 0 && lineIndex < textInputView.lineManager.lineCount else {
+            return nil
+        }
+
+        let line = textInputView.lineManager.line(atRow: lineIndex)
+        return line.location + textLocation.column
+    }

Describe alternatives you've considered

Without this we would need to either calculate line ends ourselves or introduce "proxy functions" (like GoToLine).

Can't edit hidden files

Describe the bug
A file whose name starts with a dot character or in a directory whose name starts with a dot character cannot be reached to open it for editing.

To Reproduce
Use Working Copy to clone the typical dotfiles project, e.g., GitHub.com:wolf/dotfiles.git. Many scripts live inside bash/.bash_topics.d as you can see from Working Copy. You cannot reach these files from Runestone.

Expected behavior
Runestone should either show the hidden files by default or else provide a setting to make them visible.

Crash in debug mode for Markdown files

Describe the bug

Double tapping on a line in a Markdown file hits

fatalError("\(location) is out of bounds. Valid range is \(minimumValue) - \(root.nodeTotalValue)."

Stack trace:

* thread #1, queue = 'com.apple.main-thread', stop reason = Fatal error: 14 is out of bounds. Valid range is 0 - 0. This issue is under investigation. Please open an issue at https://github.com/simonbs/Runestone/issues and include this stack trace and a sample text file if possible. This fatal error is only thrown in debug builds.
    frame #0: 0x000000018bf1d8b0 libswiftCore.dylib`_swift_runtime_on_report
    frame #1: 0x000000018bfaa550 libswiftCore.dylib`_swift_stdlib_reportFatalErrorInFile + 204
    frame #2: 0x000000018bc596d4 libswiftCore.dylib`closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> () in closure #1 (Swift.UnsafeBufferPointer<Swift.UInt8>) -> () in Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 192
    frame #3: 0x000000018bc58598 libswiftCore.dylib`Swift._assertionFailure(_: Swift.StaticString, _: Swift.String, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never + 300
  * frame #4: 0x0000000113aa44b0 Runic`RedBlackTree.node(location=14, self=0x00006000038f2c40) at RedBlackTree.swift:39:13
    frame #5: 0x0000000113b205c8 Runic`LineController.lineFragmentNode(location=14, self=0x000060000010f600) at LineController.swift:166:33
    frame #6: 0x0000000113ad35f8 Runic`TextInputStringTokenizer.isPosition(position=0x0000600003608ff0, direction=0, self=0x00006000038fed60) at TextInputStringTokenizer.swift:70:53
    frame #7: 0x0000000113ad323c Runic`TextInputStringTokenizer.isPosition(position=0x0000600003608ff0, granularity=line, direction=0, self=0x00006000038fed60) at TextInputStringTokenizer.swift:20:20
    frame #8: 0x0000000113ad4378 Runic`@objc TextInputStringTokenizer.isPosition(_:atBoundary:inDirection:) at <compiler-generated>:0
    frame #9: 0x000000011a30e448 UIKitCore`-[UIResponder(UITextInput_Internal) _findPleasingWordBoundaryFromPosition:] + 76
    frame #10: 0x000000011a30e3dc UIKitCore`-[UIResponder(UITextInput_Internal) _findBoundaryPositionClosestToPosition:withGranularity:] + 88
    frame #11: 0x000000011a2dfc98 UIKitCore`-[_UIKeyboardTextSelectionController selectPositionAtPoint:granularity:completionHandler:] + 336
    frame #12: 0x000000011a678684 UIKitCore`-[UITextSelectionInteraction tappedToPositionCursorWithGesture:atPoint:granularity:completionHandler:] + 208
    frame #13: 0x000000011a678498 UIKitCore`-[UITextSelectionInteraction _checkForRepeatedTap:gestureLocationOut:] + 812
    frame #14: 0x000000011a678bcc UIKitCore`-[UITextSelectionInteraction _handleMultiTapGesture:] + 684
    frame #15: 0x000000011a40de70 UIKitCore`-[UIApplication sendAction:to:from:forEvent:] + 96
    frame #16: 0x0000000119f25ee4 UIKitCore`-[UITextMultiTapRecognizer onStateUpdate:] + 164
    frame #17: 0x0000000119fbdce4 UIKitCore`-[UIGestureRecognizerTarget _sendActionWithGestureRecognizer:] + 76
    frame #18: 0x0000000119fc54a0 UIKitCore`_UIGestureRecognizerSendTargetActions + 88
    frame #19: 0x0000000119fc2e04 UIKitCore`_UIGestureRecognizerSendActions + 300
    frame #20: 0x0000000119fc255c UIKitCore`-[UIGestureRecognizer _updateGestureForActiveEvents] + 548
    frame #21: 0x0000000119fb6598 UIKitCore`_UIGestureEnvironmentUpdate + 2544
    frame #22: 0x0000000119fb58c8 UIKitCore`-[UIGestureEnvironment _deliverEvent:toGestureRecognizers:usingBlock:] + 276
    frame #23: 0x0000000119fb5644 UIKitCore`-[UIGestureEnvironment _updateForEvent:window:] + 156
    frame #24: 0x000000011a444594 UIKitCore`-[UIWindow sendEvent:] + 3168
    frame #25: 0x000000011a422704 UIKitCore`-[UIApplication sendEvent:] + 692
    frame #26: 0x000000012742a678 UIKit`-[UIApplicationAccessibility sendEvent:] + 92
    frame #27: 0x000000011a49eb04 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 6828
    frame #28: 0x000000011a4a0768 UIKitCore`__processEventQueue + 5612
    frame #29: 0x000000011a49970c UIKitCore`__eventFetcherSourceCallback + 220
    frame #30: 0x0000000180371070 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #31: 0x0000000180370fb8 CoreFoundation`__CFRunLoopDoSource0 + 172
    frame #32: 0x0000000180370728 CoreFoundation`__CFRunLoopDoSources0 + 232
    frame #33: 0x000000018036ae68 CoreFoundation`__CFRunLoopRun + 756
    frame #34: 0x000000018036a75c CoreFoundation`CFRunLoopRunSpecific + 584
    frame #35: 0x0000000188f60c98 GraphicsServices`GSEventRunModal + 160
    frame #36: 0x000000011a408b74 UIKitCore`-[UIApplication _run] + 868
    frame #37: 0x000000011a40cb1c UIKitCore`UIApplicationMain + 124
    frame #38: 0x000000010246bd2c GitHub`main at AppDelegate.swift:18:13
    frame #39: 0x0000000105cb5fa0 dyld_sim`start_sim + 20
    frame #40: 0x0000000105add08c dyld`start + 520

To Reproduce

Load this file https://github.com/haldun/RunestoneExample/blob/main/README.md

Expected behavior

No crash.

Screenshots

N/A.

Additional context

N/A.

Constrain Width β†’ Maximum Width units should be characetrs

Describe the bug
The Page Guide is set in characters (em widths, columns), but maximum text width is not. As a result, they cannot be set to the same values and they mismatch visually.

To Reproduce
Set Page Guide to 80 (common default of e.g. vim to wrap lines) and then try to set Maximum Width to match this.

Expected behavior
Using columns for Maximum Width.

Possibly being able to link these, e.g. by Constrain Width β†’ Follow Page Guide. But that would be a feature request :-)

Scroll to bottom

Hey.

Is there a standard way to scroll the text view to the bottom? UITextView has scrollRangeToVisible, so it can be done by [_textView scrollRangeToVisible:NSMakeRange([_textView.text length], 0)].

I tried using UIScrollview's methods, but it seems it fails to calculate the correct offset.

private func scrollToBottom() {
    let bottomOffset = CGPoint(x: 0, y: self.textView!.contentSize.height - self.textView!.bounds.height + self.textView!.contentInset.bottom)
    self.textView?.setContentOffset(bottomOffset, animated: false)
}

Curious crash when deallocating `TextInputView`

Describe the bug

I've integrated Runestone into one of my apps, where it gets displayed in a SwiftUI view using UIHostingView. I'm seeing some occasional crashes which seem to come from the OperationQueue in TreeSitterInternalLanguageMode.

I am not sure how to best address this. Any advice would be appreciated.

To Reproduce

No reliably steps to reproduce yet, as it's not deterministic.

Additional context

Sample stacktrace from an iPad Pro running iOS 15.5 (19F77), from the main thread:

Thread 0#0	(null) in objc_release ()
#1	(null) in -[NSOperationQueue dealloc] ()
#2	(null) in TreeSitterInternalLanguageMode.__deallocating_deinit ()
#3	(null) in _swift_release_dealloc ()
#4	(null) in bool swift::HeapObjectSideTableEntry::decrementStrong<(swift::PerformDeinit)1>(unsigned int) ()
#5	(null) in IndentController.__deallocating_deinit ()
#6	(null) in _swift_release_dealloc ()
#7	(null) in @objc TextInputView.__ivar_destroyer ()
#8	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#9	(null) in objc_destructInstance ()
#10	(null) in _objc_rootDealloc ()
#11	(null) in -[UIResponder dealloc] ()
#12	(null) in -[UIView dealloc] ()
#13	(null) in @objc TextView.__ivar_destroyer ()
#14	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#15	(null) in objc_destructInstance ()
#16	(null) in _objc_rootDealloc ()
#17	(null) in -[UIResponder dealloc] ()
#18	(null) in -[UIView dealloc] ()
#19	(null) in -[UIScrollView dealloc] ()
#20	(null) in @objc PlatformViewHost.__ivar_destroyer ()
#21	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#22	(null) in objc_destructInstance ()
#23	(null) in _objc_rootDealloc ()
#24	(null) in -[UIResponder dealloc] ()
#25	(null) in -[UIView dealloc] ()
#26	(null) in (anonymous namespace)::destroyGenericBox(swift::HeapObject*) ()
#27	(null) in _swift_release_dealloc ()
#28	(null) in partial apply for thunk for @callee_guaranteed (@unowned UnsafePointer<A1>) -> (@unowned Attribute<A>, @error @owned Error) ()
#29	(null) in _swift_release_dealloc ()
#30	(null) in swift_arrayDestroy ()
#31	(null) in _ContiguousArrayStorage.__deallocating_deinit ()
#32	(null) in _swift_release_dealloc ()
#33	(null) in swift_arrayDestroy ()
#34	(null) in _ContiguousArrayStorage.__deallocating_deinit ()
#35	(null) in _swift_release_dealloc ()
#36	(null) in swift_arrayDestroy ()
#37	(null) in _ContiguousArrayStorage.__deallocating_deinit ()
#38	(null) in _swift_release_dealloc ()
#39	(null) in DisplayList.ViewUpdater.deinit ()
#40	(null) in DisplayList.ViewUpdater.__deallocating_deinit ()
#41	(null) in _swift_release_dealloc ()
#42	(null) in DisplayList.ViewRenderer.deinit ()
#43	(null) in DisplayList.ViewRenderer.__deallocating_deinit ()
#44	(null) in _swift_release_dealloc ()
#45	(null) in @objc _UIHostingView.__ivar_destroyer ()
#46	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#47	(null) in objc_destructInstance ()
#48	(null) in _objc_rootDealloc ()
#49	(null) in -[UIResponder dealloc] ()
#50	(null) in -[UIView dealloc] ()
#51	(null) in _UIHostingView.__deallocating_deinit ()
#52	(null) in @objc _UIHostingView.__deallocating_deinit ()
#53	(null) in AutoreleasePoolPage::releaseUntil(objc_object**) ()
#54	(null) in objc_autoreleasePoolPop ()
#55	(null) in -[UIView dealloc] ()
#56	(null) in -[UITableViewCell .cxx_destruct] ()
#57	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#58	(null) in objc_destructInstance ()
#59	(null) in _objc_rootDealloc ()
#60	(null) in -[UIResponder dealloc] ()
#61	(null) in -[UIView dealloc] ()
#62	(null) in -[UITableViewCell dealloc] ()
#63	(null) in __RELEASE_OBJECTS_IN_THE_ARRAY__ ()
#64	(null) in -[__NSArrayM dealloc] ()
#65	(null) in -[__NSOrderedSetM dealloc] ()
#66	(null) in cow_cleanup ()
#67	(null) in -[__NSDictionaryM dealloc] ()
#68	(null) in -[UITableView .cxx_destruct] ()
#69	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#70	(null) in objc_destructInstance ()
#71	(null) in _objc_rootDealloc ()
#72	(null) in -[UIResponder dealloc] ()
#73	(null) in -[UIView dealloc] ()
#74	(null) in -[UIScrollView dealloc] ()
#75	(null) in -[UITableView dealloc] ()
#76	(null) in cow_cleanup ()
#77	(null) in -[__NSDictionaryM dealloc] ()
#78	(null) in -[UIViewController .cxx_destruct] ()
#79	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#80	(null) in objc_destructInstance ()
#81	(null) in _objc_rootDealloc ()
#82	(null) in -[UIResponder dealloc] ()
#83	(null) in -[UIViewController dealloc] ()
#84	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#85	(null) in objc_destructInstance ()
#86	(null) in _objc_rootDealloc ()
#87	(null) in cow_cleanup ()
#88	(null) in -[__NSDictionaryM dealloc] ()
#89	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#90	(null) in objc_destructInstance ()
#91	(null) in _objc_rootDealloc ()
#92	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#93	(null) in objc_destructInstance ()
#94	(null) in _objc_rootDealloc ()
#95	(null) in -[UIResponder dealloc] ()
#96	(null) in -[UIViewController dealloc] ()
#97	(null) in @objc PlatformViewHost.__ivar_destroyer ()
#98	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#99	(null) in objc_destructInstance ()
#100	(null) in _objc_rootDealloc ()
#101	(null) in -[UIResponder dealloc] ()
#102	(null) in -[UIView dealloc] ()
#103	(null) in (anonymous namespace)::destroyGenericBox(swift::HeapObject*) ()
#104	(null) in _swift_release_dealloc ()
#105	(null) in partial apply for thunk for @callee_guaranteed (@unowned UnsafePointer<A1>) -> (@unowned Attribute<A>, @error @owned Error) ()
#106	(null) in _swift_release_dealloc ()
#107	(null) in swift_arrayDestroy ()
#108	(null) in _ContiguousArrayStorage.__deallocating_deinit ()
#109	(null) in _swift_release_dealloc ()
#110	(null) in DisplayList.ViewUpdater.deinit ()
#111	(null) in DisplayList.ViewUpdater.__deallocating_deinit ()
#112	(null) in _swift_release_dealloc ()
#113	(null) in DisplayList.ViewRenderer.deinit ()
#114	(null) in DisplayList.ViewRenderer.__deallocating_deinit ()
#115	(null) in _swift_release_dealloc ()
#116	(null) in @objc _UIHostingView.__ivar_destroyer ()
#117	(null) in object_cxxDestructFromClass(objc_object*, objc_class*) ()
#118	(null) in objc_destructInstance ()
#119	(null) in _objc_rootDealloc ()
#120	(null) in -[UIResponder dealloc] ()
#121	(null) in -[UIView dealloc] ()
#122	(null) in _UIHostingView.__deallocating_deinit ()
#123	(null) in @objc _UIHostingView.__deallocating_deinit ()
#124	(null) in AutoreleasePoolPage::releaseUntil(objc_object**) ()
#125	(null) in objc_autoreleasePoolPop ()
#126	(null) in _CFAutoreleasePoolPop ()
#127	(null) in __CFRunLoopPerCalloutARPEnd ()
#128	(null) in __CFRunLoopRun ()
#129	(null) in CFRunLoopRunSpecific ()
#130	(null) in GSEventRunModal ()
#131	(null) in -[UIApplication _run] ()
#132	(null) in UIApplicationMain ()
#133	0x00000001002271a8 in main at /Users/adrian/Development/maparoni/Maparoni/helpers/OSLog+Loggers.swift:18

Go to line doesn't reveal line into view when keyboard is visible

Describe the bug
Go to line doesn't scroll the line into safe area when virtual keyboard is visible

To Reproduce

  • Open Example project
  • In MainViewController, add a new action which always runs self?.contentView.textView.goToLine(50)
  • Debug
  • Open a document which contains more than 50 lines, scroll to the very top of the document
  • Run the action
  • Line 50 is revealed to the bottom of the viewport but it's covered by the visible virtual keyboard

Expected behavior
Line 50 should be above the keyboard

Screenshots

image


Lastly thank you for the project, it reminds me of Codemirror and TextMate, both kind of become de factor standard on their own platform.

Crash with Catalyst

Describe the bug
When adding a TextView as a subview in a UIViewController in a Catalyst build there's an immediate crash when tapping into the TextView (and making first responder).

Error is:
Runestone/TextInputView.swift:1287: Fatal error: Positions must be of type IndexedPosition looks like position is nil.

Screenshots
image

Double digit line numbers truncate with a font size greater or equal to 20

Describe the bug
Double digit line numbers truncate with a font size greater or equal to 20

To Reproduce

  1. Open the Example project.
  2. Navigate to OneDarkTheme.swift.
  3. Set the font to a size of 20 or greater.
  4. Run the project
  5. Set theme to One Dark
  6. Enter enough new lines to see 10 rows

Expected behavior

  • Line numbers don't truncate.
  • Line number gutter increases in width to accommodate the double digits at the larger font size.

Screenshots
image

View for SwiftUI using `UIViewRepresentable`

Is your feature request related to a problem? Please describe.
The current TextView is built to be used with UIKit.

Describe the solution you'd like
Create a SwiftUITextView conforming to UIViewRepresentable, to wrap TextView for SwiftUI.

Additional context
I will try to make a PR to solve the problem.

Support for UIContentSizeCategoryAdjusting

Is your feature request related to a problem? Please describe.
Currently apps which utilise UILabel, UITextView and UITextField are able to support dynamic type via opt in of adjustsFontForContentSizeCategory. Once a developer enables this property, users are able to have an app experience that caters to their font size preferences for example:

  • Larger font for users with difficulty seeing.
  • Smaller font for users which want to display more content at once.

Describe the solution you'd like

  • TextView conforms to UIContentSizeCategoryAdjusting.
  • adjustsFontForContentSizeCategory can be set per TextView instance.
  • Internally this updates the fonts for both line number and text editor views.

Describe alternatives you've considered
Without modifying the original source code, a workaround currently is to set a new theme with updated fonts every time a user changes font size. This approach however doesn't seem as performant as only updating internal font properties directly.

Additional context

Add some popular tab representations

Is your feature request related to a problem? Please describe.

i like to use a different tab symbols. i don't need the triangle - but the common one.
i google for unicode tab symbol but don't find a good one ad hoc.

Describe the solution you'd like

add some default ones somehow like this

textView.tabSymbol = .triagle       // β–Έ
textView.tabSymbol = .arrow	        // ➝
textView.tabSymbol = .arrowBar      // β‡₯

Describe alternatives you've considered

or add this symbols to the docs for copy paste ;)

Additional context

SSH and Git Support

Loving the application. I periodically try to use my iPad as an on the go development machine. I try different IDE's trying to find on that fit and a lot leave much to be desired. I believe you have a winner here. The options, layout and current language support is on par with what I need.

Only thing I would need for my work flow to be complete is ssh and git/scm support. The creator of working copy also has a ssh application now. Might be worth it to reach out and see what kinda of integration options he's be interested in. I know he supports xcallback, and does a at least one text editor linked to under his app integration page. If he's willing, that'll knock out these two feature's. As I have yet to find an application that works better then working copy for git on the phone and I'm already using his ssh app, and am liking it so far.

With apples recent changes for multitasking for iPadOS. I can see a lot more people considering an iPad as a on the go development machine. So having these two features in place, or on the roadmap would help with the success of the application.

Cursor doesn't update position when changing text programmatically

Describe the bug
When updating the text directly, the cursor position doesn't update until the next keystroke. I tested on 0.2.0 and on main.

To Reproduce

  1. Type some text
  2. Have a button that calls textView.text = "" or textView.text = "any string", doesn't matter what you change it to as long as it's a different size from the original text
  3. See that the cursor doesn't move from its previous position
  4. Type another character and then the cursor will correct itself

Expected behavior

  • Changing the text programmatically should always ensure the cursor and any other UI are updated as well

Screenshots
runestone-bug

Additional context
My use case here is I have a smaller input field and I want to provide a button to let the user to easily clear it all instead of having to select all and delete.

Note: I also tried using textView.setState() instead of textView.text = in case I was using the wrong API. That resulted in the same UI bug, but additionally caused a crash on typing the next character:

Screen Shot 2022-08-12 at 9 01 30 AM

I can file that crash separately, but I figured fixing the underlying issue here would probably resolve the crash as well.

Can we find a way to collaborate on tree-sitter support?

First, huge congratulations! This is totally awesome, and shipping is a big deal.

Thanks for the SwiftTreeSitter shout-out in the acknowledgements, that's very kind of you! Do you think we could ever find a way to collaborate in that area? Seems like there's so much overlapping effort here. Perhaps https://github.com/krzyzanowskim/tree-sitter-xcframework might be a starting point?

(If you have interest in sharing why you went your own way on tree-sitter bindings, I'd love to hear. But, everyone's allowed to do their own thing and there's definitely no need justify!)

Word wrap?

Is line/word wrapping a configurable option with premium?

Thanks

Insert current timestamp

Hi @simonbs

how can i insert current date timestamp?
Can you provide button for that as well?
Automating timestamp like this would be nice :
Thu Oct 13 23:23:00 GMT 2022

API: support search in scopes

Describe the solution you'd like

Supporting range: NSRange in the Search API like snippets below

public struct SearchQuery: Hashable, Equatable {
+    /// The range to search in
+    public let range: NSRange?

-    public init(text: String, matchMethod: MatchMethod = .contains, isCaseSensitive: Bool = false) {
+    public init(text: String, matchMethod: MatchMethod = .contains, isCaseSensitive: Bool = false, range: NSRange? = nil) {

    func matches(in string: NSString) -> [NSTextCheckingResult] {
        do {
            let regex = try NSRegularExpression(pattern: annotatedText, options: regularExpressionOptions)
-            return regex.matches(in: string as String, range: NSRange(location: 0, length: string.length)) 
+            return regex.matches(in: string as String, range: range ?? NSRange(location: 0, length: string.length))
        } catch {
            #if DEBUG
            print(error)
            #endif
            return []
        }
    }

Supporting search in specified ranges would allow embedders to control where the search should happen, making it possible to build features like

  • Search in current block
  • Search in Strings/Comments

Describe alternatives you've considered

We could search in all text and then do the filtering afterwards but that would be less performant.

I can send PRs if this makes sense to you.

Don't able to open .adoc file, even with the paid version

Describe the bug
When I try to select or open an .adoc filetype (which is asciidoc's file extention). Doesn't make me do that.

To Reproduce
Create a file.adoc and try to open it.

Expected behavior
Open as plaintext like does with org files.

Screenshots
image

Additional context
I suspect isn't the editors fault, because if I create a file .adoc opens fine, it's probably an IOS file manager problem.

Can't move caret when in marked range

Describe the bug
When I use Runestone input Chinese, I press a char and marked range appear, then I change selectedRnage position by tap, bug when I continue input, the caret back to before change.

To Reproduce
Input Chinese.

Expected behavior
Like UITextView, can change selected range when in marked range

Screenshots
image

Additional context

Question Regarding KeyboardToolsView Example

In the example, you have to pass TextView into KeyboardToolsView(textView:) and then assign it to the same TextView.inputAccessoryView. Doesn't that create a retain cycle (at least that's what I get)? I wasn't able to run the Example on my machine (I will try again). Just wanna know if I am doing something wrong within my implementation.

Folding code blocks

Is your feature request related to a problem? Please describe.
Folding code blocks would be a very nice addition πŸ˜‹

Describe the solution you'd like
Clicking an arrow or plus/minus sign would toggle expanding (unfold) and collapsing (fold) of an user selected code block. This allows the user to manage large amounts of text while viewing only those subsections that are currently of interest.

Reference
https://en.wikipedia.org/wiki/Code_folding

Cannot run Example Project

When running the Example project, there is a missing Scope.swift file:

Build input file cannot be found: '/Users/darioroa/Documents/Developer/Runestone/Example/Example/Library/Scope.swift' (in target 'Example' from project 'Example')

Screenshot 2022-04-24 at 10 15 03

Option to disable font ligatures

Is your feature request related to a problem? Please describe.
"The problem" is that Runestone uses font ligatures, so it joins characters like == != <= => <==> together.

Example: (with JetBrains Mono)

image

Describe the solution you'd like
An option to disable font ligatures.

Describe alternatives you've considered
/

`textViewDidChangeSelection(_:)` called twice when deleting a character

Describe the bug
Tiny difference compared to UITextView: When deleting a character using backspace the delegate's textViewDidChangeSelection(_:) is called twice: once with length = 1 of the character to be deleted and then again with length = 0 of the actual selection.

To Reproduce
Implement TextViewDelegate, in particular the textViewDidChangeSelection(_:) method. Print textView.selectedRange in there. Then delete a character.

Expected behavior
Only called once with length = 0.

Additional context
UITextView only calls it once with length = 0.

It caused funny behaviour in my app, but I can work around this on my end by using a debounce of that signal. Might be worth fixing to align with UITextView's behaviour.

I only tested this in a Mac Catalyst app.

Stacktrace of call with length = 1:

#2	0x000000010de2ab84 in TextView.textInputViewDidChangeSelection(_:) at .../Runestone/Sources/Runestone/TextView/TextView.swift:1301
#3	0x000000010de2bf68 in protocol witness for TextInputViewDelegate.textInputViewDidChangeSelection(_:) in conformance TextView ()
#4	0x000000010ddf577c in TextInputView.selectedTextRange.setter at .../Runestone/Sources/Runestone/TextView/TextInput/TextInputView.swift:40
#5	0x000000010ddf53f8 in @objc TextInputView.selectedTextRange.setter ()
#6	0x00000001b1607238 in -[UIKBInputDelegateManager _deleteBackwardAndNotify:] ()
#7	0x0000000209779498 in -[UIKeyboardImplAccessibility deleteBackwardAndNotify:] ()
#8	0x00000001b189fb60 in -[UIKeyboardImpl performTextOperationActionSelector:] ()

Multicursor support

Multicursor support is difficult if not impossible to implement in touch-based text editors, but since iPads support both keyboard and mouse/trackpad it would be a nice feature to have when, e.g., a Magic Keyboard is attached.

Text is not laid out according to textContainerInset assigned to TextView

Runestone's TextView has textContainerInset property to configure amount of spacing around text, but the width of the text in the TextView does not match this setting. Setting textContainerInset to UIEdgeInsets.zero causes the text to flow over from trailing edge of the view. Same thing happens with and without line numbers.

To reproduce the issue, configure a TextView with some piece of text in it, so that it spans multiple lines. Enable line wrapping. Configure textContainerInset to zero margins. You should see the text flow over the trailing edge of the TextView.

IMG_5731

add some hint to indentStrategy to tabSymbol

Is your feature request related to a problem? Please describe.

i like to change tab spacing from 2 to 4. i expected a textView.tabLength but can not finde a setting ad hoc

Describe the solution you'd like

add a "link" to IndentStrategy in to the docs for tabSymbol

Describe alternatives you've considered

add an textView.tabLength - but i think this is not a good solution because it still exists as IndentStrategy

Additional context

Can't open Package.swift in Xcode 14.*

Describe the bug
Downloading the repo from GitHub and double clicking on Package.swift fails to build.

To Reproduce
Download the repo from GitHub, double click Package.swift.

Expected behavior
Builds.

Screenshots

image

I get a similar error when trying to open the Example project:

image

Additional context
I checked Getting Started but I didn't see any additional steps spelled out. I'm not sure what I'm doing wrong

Markdown syntax does not highlight

In the sample project, importing TreeSitterMarkdownRunestone package and then changing the language in MainViewController.swift:

let state = TextViewState(text: text, theme: TomorrowTheme(), language: .markdown)

Produces very simple output, indistinguishable from plain text:

Screenshot 2022-05-02 at 21 12 18

Maybe I am missing something for the purposes of implementing languages, but the docs stated this was all that was needed.

Thanks for your help, Simon πŸ˜ƒ

Cannot use from Swift Playgrounds

Is your feature request related to a problem? Please describe.
Swift Playgrounds (iPad in my case but should be applicable on Mac as well) cannot import packages that have C code included.

Describe the solution you'd like
I haven’t looked deeply into the implementation but I’d hope for either of the following:

  1. A separate package for the editor vs the language parsers
  2. Perhaps the language/C code could be provided via a pre-compiled package instead?

Again, I may be over-simplifying the solution as I’m not familiar enough to understand the problem atm.

Describe alternatives you've considered
Atm, I have to move the project to Xcode if I want to include this package (which I do), but I’m holding off on the editor bits for now because everything else can be built successfully on iPad.

Additional context
Thank you for open sourcing such a high quality editor. I’ve worked on this kind of thing before and its an truly a huge undertaking, you’ve done an incredible job! I’ve bought your Runestone app as well and its become a huge part of my daily workflow πŸ‘Œ

It doesn’t have language support for some things (like XML) but I noticed I can still open it as plain text which is great πŸ‘

Behaviour of command+left/right with line breaks

Describe the bug

I have a question about the behaviour of Runestone when having line breaks and using command + left/right to go to the beginning and end of the line. How it behaves doesn't match my expectation and makes me stop and think what happened.

Runestone jumps to the end of the "logical" or "unbroken" line, which might be further down, but I expect it to jump to the end of the "broken" line.

To Reproduce

Use Runestone and have a long line of text, enable line breaks. Press command + left/right.

I've tested this on both Catalyst and iPad and it works the same in both.

Expected behavior

The caret jumps to the end of the broken line (like Xcode)

Screen.Recording.2022-08-24.at.17.58.01.mov

Actual behavior

The caret jumps to the end of the unbroken line, which might be on the next line down (or further).

Similarly with command + left. It'd be nice if it jumped to the first non-whitespace character then, too.

Screen.Recording.2022-08-24.at.17.56.55.mov

Is this expected behaviour? Is there some way to customise this already? While trying to see why this happened, I noticed that it appears to be UIKit setting the selected text range -- see below. If you agree that this behaviour should be changed, @simonbs , but you don't have time to tackle this, you can also point me in the direction to fix it myself and I'll give it a shot. I tried but couldn't figure out where's the logical place to hook into.

// MARK: - UITextInput
    var selectedTextRange: UITextRange? {
        get {
            if let range = _selectedRange {
                return IndexedRange(range)
            } else {
                return nil
            }
        }
        set {
            // We should not use this setter. It's intended for UIKit to use.
            // It'll invoke the setter in various scenarios, for example when navigating the text using the keyboard.
            let newRange = (newValue as? IndexedRange)?.range
            if newRange != _selectedRange {
                shouldNotifyInputDelegateAboutSelectionChangeInLayoutSubviews = true
                _selectedRange = newRange
                delegate?.textInputViewDidChangeSelection(self)
            }
        }
    }

Property to set TextView editable or not

UITextView has a isEditable property which makes it possible to disable editing for a specific instance. Runestone's TextView mostly offers the same customisation as UITextView but is missing this setting. It would be quite important for those cases where a TextView only needs to display code or other syntax highlighted text without allowing editing. It would be great to have isEditable property in TextView.

Perhaps this could be implemented by, under the hood, simply not becoming first responder on user interaction, whenever the editable boolean is set to false.

It might actually be nice to have some options for this non-editable mode. Having a mode where the view can accept first responder, with a visible cursor that can be moved around with something like an iPad Magic Keyboard, would be really nice to make it easier to copy paste parts of the text. Another mode could make the text view display only, in a way where it does not accept focus.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.